需求:积分气泡实现类蚂蚁森林动态效果

UI图
在这里插入图片描述

技术栈:vue,ts

需求分析
1.在特定的区域内随机产生积分气泡,气泡之间不重合,气泡与总能量球之间不重合;
2.每个能量球上下浮动,点击后向总能量球移动逐渐消失

实现思路
1.不重合能量气泡的产生:随机数产生能量气泡的球心,递归判断产生的球心是否可用,若可用则push到数组,否则重新递归;
2.能量球的动态效果:通过css3的animation实现。
思路来源于大佬:思路来源

主要代码
获取有效气泡球心坐标:

 // 产生随机数
 _rnd(n: number, m: number): number {
    const random = Math.floor((Math.random() * (m - n + 1)) + n);
    return random;
  }
  /**
   * 判断两圆心之间距离是否大于直径 或者球心距totalBall球心距离小于
   * p1 -- 已创建可用Ball的ballList中item
   * p2 -- 当前创建ball
   * distanceToBall -- 两个ball球心之间最小距离
   */
  _getDistanceForBallCenter(p1: object, p2: object, distanceToBall: number): boolean {
    const dx = Math.abs(p2.x - p1.x);
    const dy = Math.abs(p2.y - p1.y);
    const disToBall = Math.sqrt((dx ** 2) + (dy ** 2));
    return disToBall > distanceToBall;
  }
  // 产生有效气泡球心坐标
  _getSingleIntegralBallCenter(arrList: Array<object>): any {
    const currentArrList: any = [];
    arrList.forEach((arrItem) => { // 过滤掉上一页的
      if (!arrItem.click) {
        currentArrList.push(arrItem);
      }
    });
    const _ballCenterPos = {
      x: this._rnd(0.1 * this.viewW, 0.9 * this.viewW),
      y: this._rnd(0.1 * this.viewH, 0.6 * this.viewH)
    };
    const _noCoverToTal = this._getDistanceForBallCenter(
      this.totalCenterPos,
      _ballCenterPos,
      this.distanceToTotal
    );
    if (_noCoverToTal) {
      if (currentArrList && currentArrList.length > 0) {
        let isFlag = true; // 该pos是否全部不重合标志
        currentArrList.forEach((listItem: any) => {
          const _noCover = this._getDistanceForBallCenter(
            listItem,
            _ballCenterPos,
            this.distanceToBall
          ); // 该pos是否与该ball不重合
          if (!_noCover) {
            isFlag = false;
          }
        });
        if (isFlag) {
          return _ballCenterPos;
        }
        return this._getSingleIntegralBallCenter(this.ballCenterPosList);
      }
      return _ballCenterPos;
    }
    return this._getSingleIntegralBallCenter(this.ballCenterPosList);
  }

渲染能力气泡:

<!-- 类蚂蚁森林HTML -->
    <div
      class="antForest-container"
      ref="antForest"
    >
      <template v-if="integralAvailabel && nodeList && nodeList.length > 0">
        <template v-if="nodeList && nodeList.length > 0">
          <div id = "antForest-ball__container">
            <div
              v-for="(ballItem, ballIndex) in nodeList"
              :key="ballIndex"
            >
              <!-- 多一层实现悬浮晃动效果 -->
              <div
                :id="`antForest-ball__item_${ballIndex}`"
                @click="clickIntegralBall(ballIndex, ballItem)"
              >
                <div class="antForest-ball__item">
                  <div class="ball-item__ball ds-flex align-center justify-center">
                    <span
                      class="ball-item__count font-number-medium"
                    >{{ballItem.scoreValue > 0 ? `+${ballItem.scoreValue}` : ballItem.scoreValue}}</span>
                  </div>
                  <div class="ball-item__type">{{ballItem.description}}</div>
                </div>
              </div>
            </div>
          </div>
        </template>
        <div v-if="userIntegralInfo.totalScore === 0" class="noIntegral-tip">
          <div class="noIntegral-tip__topBox">快去完成任务来获取积分吧!</div>
          <div class="noIntegral-tip__bottomBox"></div>
        </div>
        <div class="antForest-container__totalTegral">
          <span class="totalTegral-num font-number-medium">{{userIntegralInfo.totalScore || 0}}</span>
          <img
            v-if="showGetAllIcon"
            class="totalTegral-getAll__img"
            id="totalTegral-getAll__img"
            @click="clickGetAll"
            src="https://img1.tuhu.org/PeccancyCheXingYi/FobgTRJWhXW4LvPOA5xCVAmWG7Rr_w408_h128.png@100Q.png"
          />
        </div>
      </template>
	</div>
	
// js
// 一页面十个积分气泡
 createIntegralBallWall() {
   if (this.nodeList && this.nodeList.length > 0) {
     this.nodeList.forEach((item, index) => {
       const pos = this._getSingleIntegralBallCenter(this.ballCenterPosList);
       this.ballCenterPosList.push(pos);
       this._renderBallPos(index, pos);
     });
   }
 }

// 每个积分气泡的定位
_renderBallPos(index: number, pos: object) {
   const antForestBallItem = document.getElementById(`antForest-ball__item_${index}`);
   if (antForestBallItem) {
     (antForestBallItem as any).className = 'antForest-ball__item_container';
     (antForestBallItem as any).style.opacity = 1;
     (antForestBallItem as any).style.position = 'absolute';
     (antForestBallItem as any).style.left = `${((pos.x - 42.5) / this.viewW) * 100}%`;
     (antForestBallItem as any).style.top = `${((pos.y - 42.5) / this.viewH) * 100}%`; // 使用%定位
   }
 }
     

积分气泡的动态效果:

.antForest-ball__item {
    position: relative;
    animation: ballShaking 3s infinite;
    font-size: 28px;
    color: rgba(255, 255, 255, 1);
    line-height: 35px;
}
@keyframes ballShaking {
  0% {
    position: relative;
    top: 0;
  }
  50% {
    position: relative;
    top: 10px;
  }
  100% {
    position: relative;
    top: 0px;
  }
}

点击积分气泡效果:

// 节流
@throttle(800)
// 气泡点击
 clickIntegralBall(ballIndex: number, ballItem: object) {
   if (!this.getAllClicking && !ballItem.click) {
     this.singleClicking = true;
     this.nodeList[ballIndex].click = true;
     this.timer = setTimeout(() => {
       (document.getElementById(`antForest-ball__item_${ballIndex}`) as any).className = 'clickBall';
       (document.getElementById(`antForest-ball__item_${ballIndex}`) as any).style.opacity = 0;
     }, 300);
     const collectIntegralList = [];
     collectIntegralList.push(ballItem.operationLogId);

     // !!!不能remove Dom
     this.timer = setTimeout(() => {
       const params = {
         operationLogIds: [...collectIntegralList]
       };
       this.collectIntegralBall(params);
     }, 500);
   } else {
     console.log('该积分已经领取过啦或者正在一键领取');
   }
 }
// css
.clickBall {
  animation: ballClick 3s ease;
}
@keyframes ballClick {
  0% {
    position: absolute;
    opacity: 1;
  }
  50% {
    position: absolute;
    opacity: 0.4;
  }
  75% {
    position: absolute;
    opacity: 0.1;
  }
  100% {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%);
    opacity: 0;
  }
}

若有错误或者建议,欢迎指出~

Logo

前往低代码交流专区

更多推荐