像素

九宫格抽奖的两种实现方案

九宫格抽奖,实现跑马灯的效果,要求启动后要有加速、匀速和减速的过程。 这是图片

  • 根据设置的顺序运动。 要有速度变换的过程
  • 运动过程中当前格子要有操作,比如边框、高亮。

第一种方案 setTimeout

通过修改 setTimeout 的第二个参数,控制每次运动的时间间隔,从而达到加减速的效果。

  • 采用环形链表的结构来描述抽奖运动的整个过程,这样好处是只需要每次移动向下一个就好
/**
 *
 * @param {*} DataArr 奖品数组
 * @param {*} count 圈数
 * @param {*} id 中奖id
 * @param {*} cb 每走一格的回调
 * @param {*} end 结束回调
 * @returns
 */
function start(DataArr, count, id, cb, end) {
  console.log("中奖id", id);
  let i = 1; // 初始值为1 代表当前在第一个
  let time,
    current = 0;
  // 最大速度 150
  let timeout = 300; // 走一格所花的时间,时间越大,速度越慢。

  for (let m = 0; m < DataArr.length; m++) {
    // 找到圈数之后要走的步数
    if (DataArr[m] == id) {
      break;
    }
  }
  // 创建链表
  let data = Array(DataArr.length)
    .fill(0)
    .map((e, i) => ({ order: i }));
  for (let i = 0; i < DataArr.length; i++) {
    // 格式化奖品 环形链表
    data[i].next = i < DataArr.length - 1 ? data[i + 1] : data[0];
  }

  // 定位到抽中的奖品
  let n = 0;
  let tem = data[0];
  while (true) {
    console.log("抽中的奖品", id);
    if (tem.order === id) break;
    tem = tem.next;
    n++;
    if (n === data.length) {
      console.error(`奖品${id}不存在`);
      return;
    }
  }

  let currentObj = data[0];
  let allcount = DataArr.length * count + n; // 计算需要走多少格
  cb(currentObj); // 初始化位置
  function step() {
    // 结束条件
    if (i > data.length * count && currentObj.order == id) {
      clearInterval(time);
      time = null;
      end();
      return;
    }
    // 加速
    if (i < allcount / count) timeout = timeout - (160 / allcount) * count;
    // 减速
    if (i > ((count - 1) * allcount) / count)
      timeout = timeout + (500 / allcount) * count;
    if (i == allcount - 2) timeout = 600;
    if (i == allcount - 1) timeout = 1000;

    currentObj = currentObj.next; // 获取当前停留格子的数据,
    cb(currentObj);
    i++;
    setTimeout(step, timeout);
  }
  time = setTimeout(step, timeout);
}
export default start;

第二种方案 requestAnimationFrame

requestAnimationFrame 方法告诉浏览器您希望执行动画,并请求浏览器在下一次重新绘制之前调用指定的函数来更新动画。

它返回一个 id,标示当前的回调。用法与 settimeout 类似可以通过cancelAnimationFrame 用于取消这个函数的执行。

基本实现思路跟第一种方案类似,在于把每一步区分成多个帧,相隔一定的帧数走一格。 通过控制每走一格所相隔的帧数,从而达到视觉上加速、减速的效果。

class LuckCtrl {
  private data: Record<string, any>[];
  private maxSpeed: number;
  private cycleNumber: number;
  private minSpeed: number;
  Raf?: number;

  /**
   *
   * @param DataArr
   * @param cycleNumber
   * @param minSpeed
   */
  constructor(
    DataArr: number[],
    cycleNumber: number = 7,
    minSpeed: number = 10
  ) {
    this.data = Array(DataArr.length)
      .fill(0)
      .map((e, i) => ({ order: i }));
    this.maxSpeed = 4; // 最大速度
    this.cycleNumber = cycleNumber; // 圈数
    this.minSpeed = minSpeed; // 最小速度

    for (let i = 0; i < DataArr.length; i++) {
      // 格式化奖品 环形链表
      this.data[i].next =
        i < DataArr.length - 1 ? this.data[i + 1] : this.data[0];
    }
  }

  /**
   * 运动开始
   * @param id 抽中的奖品id
   * @param running 运动中回调
   * @param runend 运动结束回调
   * @returns
   */
  run(
    id: number,
    running: (k: Record<string, any>) => void,
    runend: () => void
  ) {
    let counter = 0, // 记速器
      current = 0, // 调速器 控制区内间速度
      currentObj = this.data[0];

    let n = 0;
    let tem = this.data[0];
    while (true) {
      // 定位到抽中的奖品
      console.log("抽中的奖品", id);
      if (tem.order === id) break;
      tem = tem.next;
      n++;
      if (n === this.data.length) {
        console.error(`奖品${id}不存在`);
        return;
      }
    }

    let allCount = this.cycleNumber * this.data.length + n; // 总运动区间
    let addSpace = this.minSpeed - this.maxSpeed; // 加速区间
    let reduceSpace = allCount - addSpace; // 减速区间

    /**
     * raf 每次执行回调
     * @returns
     */
    const step = () => {
      // 加速
      if (counter < addSpace) {
        if (current < Math.pow(this.minSpeed - counter, 2)) {
          current += this.minSpeed / 2;
        } else {
          current = 0;
          counter++;
          currentObj = currentObj.next;
          running(currentObj);
          console.log("加速阶段");
        }
      }

      // 匀速
      if (counter >= addSpace && counter < reduceSpace) {
        if (current < this.maxSpeed) current++;
        else {
          // 计数清零
          current = 0;
          counter++;
          currentObj = currentObj.next;
          running(currentObj);
          console.log("匀速阶段");
        }
      }

      // 减速
      if (counter >= reduceSpace && counter < allCount) {
        if (Math.sqrt(current) <= this.minSpeed - (allCount - counter)) {
          current += 2;
        } else {
          current = 0;
          counter++;
          currentObj = currentObj.next;
          running(currentObj);
          console.log("减速阶段");
        }
      }

      // 停止
      if (counter >= allCount) {
        runend();
        console.log("停止");
        this.offRaf(); // 关闭raf
        return;
      }
      this.Raf = requestAnimationFrame(step);
    };
    running(currentObj); // 初始化抽奖位置
    console.log("抽奖开始");
    this.Raf = requestAnimationFrame(step);
  }
  offRaf() {
    console.log("关闭raf");
    this?.Raf && cancelAnimationFrame(this.Raf);
  }
}