红包雨实现(优化版)

文章介绍了红包雨效果的优化版本,包括使用spritejs框架代替DOM操作,提升点击灵敏度,解决重叠问题,以及添加暂停和继续功能。同时,文章指出了可能存在的红包位置重叠和点击灵敏度调整的不足,并提供了示例代码展示如何实现这些优化。

红包雨实现(优化版)

实现效果在这里插入图片描述

优化内容

1、优化了点击灵敏度问题,将click改成touchstart(为啥要修改下面有说)
2、优化了重叠严重的问题
3、使用spritejs框架生成红包,不再操作原生dom
4、增加了暂停游戏、继续游戏的功能
5、支持配置多种样式大小的红包
6、增加了一下其他配置功能去控制界面元素的相关显示

使用框架

spritejs
http://spritejs.com/#/

实现原理

第一步:根据传入的红包尺寸数组计算出平均宽度和平均长度(init方法)
第二步:调用createRedPacketRain方法创建场景
第三步:通过create给定红包的位置(重点)
该方法的实现实际上就是根据平均宽度和平均长度得出屏幕上一行和一列分别能放置多少元素
然后随机取行列位置(取出来后将该位置删除 则下一个元素无法再次放置在这个位置),生成红包放置在屏幕上
第三步:得到位置,调用createRedPacket在屏幕上生成红包并设置触碰事件与相关动画
第四步:当单个红包被点击或者下落到底部,从屏幕上移除这个元素,重新调用create()方法在屏幕上随机生成一个元素

主要就是弄清楚spritejs的使用已经生成位置的方法就可以很快的理解这个组件的实现了

不足之处

1、为了避免红包的左右位置每次都固定间隔无法错落,设置了一个左右的随机偏移,这就可能导致红包的部分重叠
2、因为每次点击和红包下落到底部都要从新在屏幕取一个位置生成,但这个时候没法判断取的这个点是不是有红包,所以有可能导致红包重叠
3、之前因为是用的click时间(移动端为了监控双击事件,所以click时间会有300ms的延时,但是我看了一些支付之类的抢能量的案例 也是不给过快点击的 可是产品他们觉得这样用户体验不好,快速点击的时候有写红包抢不到,于是改成了touchStart事件 但我觉得这种过于灵敏,后续若真需要控制红包点击的灵敏度,可修改事件触发,用touch的事件模拟一个点击事件出来)

使用示例

import React,{FC, useEffect, useRef, useState} from "react";
import styles from './index.less'
import RedpacketRain,{IHandle as RedpacketRainHandle} from '@/components/redpacketRain'

const Test:FC<any> = (props)=>{

  const shareConfigRef = useRef<RedpacketRainHandle>(null);

  const gameOver = (score)=>{
      console.log("游戏结束",score)
  } 

  // 重启游戏
  const again = ()=>{
    shareConfigRef.current?.restart()
  }

  // 关闭弹窗
  const handleClose =()=>{

  }

  return(
  <div className={styles.test} >
      <RedpacketRain 
         redpacketArr={[redpacket1,redpacket2,redpacket3,redpacket4,redpacket5]}
         redpacketSize={new Array(5).fill({redpacketWidth:138/2.3,redpacketHeight:213/2.3})}
          ref={ConfigRef}
          time={30} 
          gameOver={gameOver}
          basenum={10}
          visible={gameVisible}
          isincline={false}
          speed={3000}
          needCloseBtn={false}
          addOne={addOne}
          needReadygo={false}
      />
  </div>)
}

export default Test

代码实现

import React, {
  FC,
  useEffect,
  useRef,
  useState,
  forwardRef,
  useImperativeHandle,
} from "react";
import { Gradient, Group, Label, Scene, Sprite } from "spritejs";
import CommonPopUp from "../commonPopUp"; //公司封装的弹窗组件
import styles from "./index.less";
import addOnepng from "./img/addOne.png";
import ready from "./img/ready.png";
import go from "./img/go.png";
import { Toast } from "antd-mobile";

interface Props {
  /**
   * 是否需要Ready go提示
   */
  needReadygo?: boolean;
  /**
   * 红包素材数组
   */
  redpacketArr: Array<any>;
  /**
   * +1素材
   */
  addOne?: any;
  /**
   * 红包素材大小
   */
  redpacketSize: Array<{ redpacketWidth: number; redpacketHeight: number }>;
  /**
   * 红包下落速率种子配置 数值越大 下落速度越慢
   */
  speed: number;
  /**
   * //红包基数
   * 可通过这个参数控制屏幕上显示的红包数量
   */
  basenum: number;
  /**
   * 游戏时间
   */
  time: number;
  /**
   * 总参与机会
   */
  joinTime?: number;
  /**
   * 剩余参与机会
   */
  remainJoinTime?: number;
  /**
   * 该红包雨弹窗是否可见
   */
  visible: boolean;
  /**
   * 红包是否倾斜 默认值false
   */
  isincline?: boolean;
  /**
   * 游戏结束后的回调
   * 传入游戏得分
   */
  gameOver: (score) => void;
  /**
   * 是否需要关闭按钮
   */
  needCloseBtn: boolean;
  /**
   * 关闭游戏的函数
   * 传入游戏得分
   */
  handleClose?: (score?: number) => void;
}

export interface IHandle {
  /**
   * 重启游戏
   */
  restart: () => void;

  /**
   * 暂停游戏
   */
  paused: () => void;

  /**
   * 继续游戏
   */
  continute: () => void;
}

const RedpacketRain = forwardRef<IHandle, Props>((props, ref) => {
  const {
    handleClose,
    gameOver,
    basenum,
    time,
    joinTime,
    remainJoinTime,
    visible,
    speed,
    isincline,
    redpacketArr,
    redpacketSize,
    needCloseBtn,
    addOne,
    needReadygo,
  } = props;
  const [myScore, setMyScore] = useState(0);
  const scoreRef = useRef<any>(0);
  scoreRef.current = myScore;
  const [countDown, setCountDown] = useState(0);
  const countDownRef = useRef<any>(0);
  countDownRef.current = countDown;

  const timerRef = useRef<any>();

  const containerRef = useRef<any>();

  const spriteRef = useRef<any>();

  const [readygoStep, setreadygoStep] = useState(0);
  const [H, setH] = useState<any>(0);
  const [W, setW] = useState<any>(0);

  useEffect(() => {
    if (visible) {
      // 先ready go 在执行begin函数
      if (needReadygo) {
        setreadygoStep(1);
        setTimeout(() => {
          setreadygoStep(2);
          setTimeout(() => {
            setreadygoStep(0);
            begin();
          }, 700);
        }, 1000);
      } else {
        begin();
      }
    } else {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
      setMyScore(0);
      setCountDown(0);
      init();
    }
  }, [visible]);

  useEffect(() => {
    init();
  }, []);

  const begin = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
    setCountDown(time);
    setMyScore(0);
    // 设置倒计时
    timerRef.current = setInterval(() => {
      if (countDownRef.current <= 0) {
        clearInterval(timerRef.current);
        setTimeout(() => {
          gameOver(scoreRef.current);
        }, 500);
        return;
      }
      setCountDown(countDownRef.current - 1);
    }, 1000);

    createRedPacketRain(basenum);
  };

  // 初始化一张图(二维数组)格子法
  const init = () => {
    let sumH = 0;
    let sumW = 0;
    redpacketSize.map(item => {
      sumW += item.redpacketWidth;
      sumH += item.redpacketHeight;
    });
    // 素材宽高(取平均值)
    const H = Math.ceil(sumH / redpacketSize.length) + 10;
    const W = Math.ceil(sumW / redpacketSize.length);

    setH(H);
    setW(W);
  };

  const createRedPacketRain = basenum => {
    if (!basenum || basenum <= 0) {
      Toast.info("请设置有效的红包基数");
      return;
    }
    const container = containerRef.current;
    if (!container) return;

    const width = container.clientWidth;
    const height = container.clientHeight;
    // 创建场景
    const scene = new Scene({
      container,
      width: width,
      height: height,
      mode: "stickyTop",
    });

    // 层级
    const layer = scene.layer();
    spriteRef.current = layer;

    // 创建红包
    const createRedPacket = (
      src,
      redpacketWidth,
      redpacketHeight,
      xStart,
      yStart
    ) => {
      //  创建包裹层
      const wrapper = new Group({
        pos: [xStart, yStart], //初始化位置
      });

      // 红包图片
      const rp = new Sprite(src);
      // +1提示
      const addone = new Sprite(addOne || addOnepng);
      // 光晕
      const haolo = new Sprite();

      // 红包属性设置
      rp.attr({
        pos: [0, 0], //图片位置
        size: [redpacketWidth, redpacketHeight], //图片大小
        transform: isincline ? `rotate(20deg)` : `rotate(0deg)`, // 控制倾斜
      });

      // +1属性设置
      addone.attr({
        pos: [126 / 1.5, 0],
        size: [25, 25],
        opacity: 0,
        zIndex: -1,
      });

      // 光晕属性设置
      //@ts-ignore
      haolo.attr({
        size: [80, 80],
        borderRadius: 40,
        pos: [0, 0],
        opacity: 0,
        zIndex: -1,
        bgcolor: new Gradient({
          vector: [40, 40, 10, 40, 40, 80],
          /*
          vector: [x0,y0,r0,x1,y1,r1],
          ● x0:定义渐变的开始圆的 x 坐标

          ● y0:定义渐变的开始圆的 y 坐标

          ● r0 :定义开始圆的半径

          ● x1:定义渐变的结束圆的 x 坐标

          ● y1:定义渐变的结束圆的 y 坐标

          ● r1:定义结束圆的半径
          */
          colors: [
            { offset: -0.2, color: "#00000000" },
            { offset: 0.3, color: "#b14a1ecf" },
            { offset: 0.8, color: "#f4e259c2" },
          ],
        }),
      });

      // 注册事件监听(touch事件)
      rp.addEventListener("touchstart", () => {
        if (countDownRef.current <= 0) {
          return;
        }

        setMyScore(scoreRef.current + 1);
        rp.setAttribute("width", 0);
        wrapper.setAttribute("zIndex", -99); //设置为 -99 不挡住别的元素
        addone
          .animate(
            [
              { pos: [126 / 1.5, 0], opacity: 1, scale: 1 },
              { pos: [126 / 1.5, -300], opacity: 0, scale: 1.3 },
            ],
            {
              duration: 1000,
              fill: `forwards`, // 动画定格在最后一帧
            }
          )
          .finished.then(() => {
            addone.setAttribute("width", 0);
            wrapper.setAttribute("width", 0);
          });

        haolo
          .animate(
            [
              { opacity: 1, scale: 1 },
              { opacity: 0, scale: 1.5 },
            ],
            {
              duration: 500,
              fill: `forwards`, // 动画定格在最后一帧
            }
          )
          .finished.then(() => {
            haolo.setAttribute("width", 0);
            wrapper.remove();
            create(1);
          });
      });

      // 外层属性设置
      // 红包下落动画
      const animate = wrapper.animate(
        [{ pos: [xStart, yStart] }, { pos: [xStart, height] }],
        {
          duration: speed + (-yStart / height) * speed,
          fill: `forwards`, // 动画定格在最后一帧
          easing: "linear",
        }
      );

      animate.finished.then(() => {
        if (countDownRef.current <= 0) {
          return;
        }
        wrapper.remove();
        create(1);
      });

      wrapper.appendChild(addone);
      wrapper.appendChild(haolo);
      wrapper.appendChild(rp);
      layer.append(wrapper);
    };

    // 生成位置代码
    const create = basenum => {
      // 随机取一个红包
      const key = Math.floor(Math.random() * redpacketArr.length);
      const src = redpacketArr[key];
      const size = redpacketSize[key];
      const { redpacketWidth, redpacketHeight } = size;
      //当放置的元素的宽高大于浏览器窗口的宽高时,直接返回
      if (redpacketWidth > width || redpacketHeight > height) {
        return false;
      }

      // 一些计算
      // 横向可以分成多少等分
      const xNum = Math.floor(width / W);
      // 纵向可以分成多少等分
      const yNum = Math.floor(height / H);

      const allNum = xNum * yNum; //浏览器窗口内总共可以放置元素的个数
      //当需要放置的元素的个数超过浏览器窗口内总共可以放置的元素的个数时,则返回
      let num = basenum;

      if (basenum >= allNum) {
        console.log(
          "显示基数已经超出屏幕所能放置的元素个数了,设置新的基数值",
          allNum - 1
        );
        num = allNum - 1;
      }

      let _tmpArray: Number[] = [];
      for (let i = 0; i < allNum; i++) {
        _tmpArray.push(i);
      }

      let xStart = 0,
        yStart = 0;
      while (num) {
        const pointer = Math.floor(Math.random() * allNum); //向下取整取出0到allnum之间的任意一个整数
        //如果数组_tmpArray中不存在第pointer值,则继续
        if (!_tmpArray[pointer]) {
          continue;
        }
        delete _tmpArray[pointer]; //删除数组_tmpArray中第pointer个值
        yStart = parseInt(pointer / xNum + "", 10) * H + H;
        const xramdom = Math.round(Math.random() * (W * 0.4)) - W * 0.2;
        xStart = (pointer % xNum) * W + xramdom;
        // 在屏幕上创建这个元素
        createRedPacket(src, redpacketWidth, redpacketHeight, xStart, -yStart);
        num--;
      }
    };

    create(basenum);
  };

  // 暂停
  const paused = () => {
    clearInterval(timerRef.current);
    spriteRef.current.timeline.playbackRate = 0;
  };

  // 继续
  const continute = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
    // 重新开始倒计时
    spriteRef.current.timeline.playbackRate = 1;
    // 设置倒计时
    timerRef.current = setInterval(() => {
      if (countDownRef.current <= 0) {
        clearInterval(timerRef.current);
        setTimeout(() => {
          gameOver(scoreRef.current);
        }, 500);
        return;
      }
      setCountDown(countDownRef.current - 1);
    }, 1000);
  };

  useImperativeHandle(ref, () => ({
    restart: begin,
    continute,
    paused,
  }));

  return (
    <CommonPopUp visible={visible}>
      <div className={styles.main}>
        {readygoStep === 1 && <img className={styles.ready} src={ready} />}
        {readygoStep === 2 && <img className={styles.go} src={go} />}
        {!readygoStep && (
          <div className={styles.topWrapper}>
            {needCloseBtn ? (
              <div
                className={styles.back}
                onClick={() => handleClose && handleClose(myScore)}
              />
            ) : null}

            <div
              className={styles.tip}
              style={{ marginTop: needCloseBtn ? "-0.5rem" : "0.5rem" }}
            >
              <div className={styles.jointime}>
                {joinTime && remainJoinTime ? (
                  <span>
                    参与机会:{remainJoinTime}/{joinTime}
                  </span>
                ) : null}
              </div>
              <div className={styles.time}>{countDown}S</div>
              <div className={styles.num}>分数:{myScore}</div>
            </div>
          </div>
        )}
        <div className={styles.redpacket} ref={containerRef} />
      </div>
    </CommonPopUp>
  );
});

export default RedpacketRain;
.main {
  position: relative;
  z-index: 99;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.4);
  .redpacket {
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    left: 0;
  }
  .ready {
    width: 4.06rem;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%);
  }
  .go {
    width: 2.23rem;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%);
  }

  .topWrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    .back {
      width: 0.26rem;
      height: 0.38rem;
      background-repeat: no-repeat;
      background-size: contain;
      background-image: url(./img/back.png);
      margin: 0.4rem 0.3rem;
    }
    .tip {
      display: flex;
      justify-content: space-between;
      align-items: center;
      color: #ffedd3;
      font-size: 0.3rem;
      padding: 0 0.3rem;
      margin-top: 0.5rem;
      div {
        width: 30%;
      }
      .time {
        width: 1.7rem;
        height: 1.7rem;
        border-radius: 50%;
        border: 0.03rem #ffedd3 solid;
        text-align: center;
        line-height: 1.7rem;
        font-size: 0.6rem;
      }
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值