Animejs-🎉礼花彩带散落

805 阅读3分钟

实现效果

知识点

背景礼花飘落效果

实现方式其实有2种

  1. 通过创建标签,给随机动画(原生js可用)loop重复执行;
  2. 优先固定标签数量,给随机动画(适用于vue:v-for,小程序:wx:for)loop重复执行,小程序上如何实现会在下面介绍;

createElement创建标签

image.png

  const [$body] = utils.$('.body')
  const bgFlowerNum = 200 // 背景礼花数量
  const bgFlowerSize = 10 // 背景礼花大小
  ...
  // 创建公用小花瓣
  const createFireworksDot = ({ top, left, scale }) => {
    const $el = document.createElement('div')
    scale = scale || utils.random(0.2, 1.2, 1)
    // 给标签设置样式属性
    utils.set($el, {
      width: `${bgFlowerSize * scale}px`, // 随机定义大小尺寸
      height: `${bgFlowerSize * scale}px`,
      position: 'absolute', // 定义初始化位置信息
      top,
      left,
      transformStyle: 'preserve-3d', // 定义3D旋转效果
      // 随机获取一种彩带颜色
      background: utils.randomPick([
        '#dce90d',
        '#0de9be',
        '#0dbce9',
        '#e9530d',
        '#e90d59',
        '#ffffff'
      ]),
      // 随机定义彩带是正方形还是圆形
      borderRadius: utils.randomPick(['50%', '0%']),
      userSelect: 'none'
    })
    // $el.classList.add('bg-flower') // 可忽略,因为已经提前设置好样式属性,其次通过优先定义class的方式不会生效
    return $el
  }
  ...
  // 根据设置的最大礼花数量,初始化标签
    const createBgFlower = () => {
    const bounds = $body.getBoundingClientRect()
    const { width, height, left, top } = bounds
    for (let i = 0; i < bgFlowerNum; i++) {
      const scale = utils.random(0.2, 1.2, 1)
      // 初始化位置坐标,左右偏移为 0~body最大宽度,上下偏移为 -50~-20
      const $el = createFireworksDot({
        left: `${utils.random(0, width)}px`,
        top: `${utils.random(-50, -20)}px`,
        scale
      })
      $body.appendChild($el)
      ...
    }
  }

添加anime动画让彩带动起来

anime2.gif

  // 创建背景礼花下落动画
  const createBgFlower = () => {
    ...
    for (let i = 0; i < bgFlowerNum; i++) {
      ...
      $body.appendChild($el)
      const animation = animate($el, {
        translateX: [
          {
            from: utils.random(-200, 200),
            to: utils.random(-10, 10),
            ease: 'out'
          },
          { to: utils.random(-100, 100), ease: 'inOut(2)' }
        ],
        translateY: [{ to: utils.random(height + 50, height + 100), ease: 'inOut(2)' }],
        duration: (6000 - 2000 * scale) / bgFlowerSpeed, // 根据彩带大小控制运动速度,彩带越大下落越快
        rotate: `${utils.random(1, 3)}turn`, // 旋转角度 1turn = 360°
        rotateX: `${utils.random(1, 3)}turn`,
        rotateY: `1turn`,
        //   scale: [1, 1.2, 1, 0.1],
        ease: 'inOut(2)',
        delay: utils.snap(utils.random(-5000, 5000), 500) / bgFlowerSpeed,
        loop: true // 重复执行
      })
      bgFlowerArr.push({ $el, animation }) //将标签和动画添加到bgFlowerArr数组中,方便后期重置动画
    }
  }

窗口变化重新渲染动画

anime3.gif

  let bgFlowerArr = []; // 彩带标签动画集合数组
  ...
  const draw = () => {
    // 每次重绘前清空背景元素和动画
    bgFlowerArr.forEach(({ $el, animation }) => {
      animation.revert()
      $body.removeChild($el)
    })
    bgFlowerArr = []
    createBgFlower()
  }
   // 监听窗口尺寸改变重绘
  window.addEventListener('resize', () => {
    draw()
  }) 

点击按钮喷洒礼花🎉

前期工作准备充分后,点击按钮喷洒的效果也就方便实现了

初始化礼花位置

image.png

  <div class="body">
    <div class="anime-box">
      <div class="button">🎉 click me</div>
    </div>
  </div>
  const [$animeBox] = utils.$('.anime-box')
  ...
  // 点击按钮时初始化礼花位置
  const createButtonAnimation = () => {
    const bounds = $animeBox.getBoundingClientRect()
    const { width, height, left, top } = bounds
    // 这里的位置取按钮的中心位置
    const hw = width / 2
    const hh = height / 2
    // 这里的喷洒数量可自行控制
    for (let i = 0; i < 20; i++) {
      const $el = createFireworksDot({
        left: `${hw}px`,
        top: `${hh}px`
      })
      $animeBox.appendChild($el)
    }
  }

添加anime动画执行喷洒效果

anime4.gif

  // 点击按钮时执行礼花爆开动画
  const createButtonAnimation = () => {
    ...
    for (let i = 0; i < 20; i++) {
      const $el = createFireworksDot({
        left: `${hw}px`,
        top: `${hh}px`
      })
      $animeBox.appendChild($el)
      const scale = utils.random(0.2, 1.2, 1)
      animate($el, {
        translateX: [
          {
            to: utils.random(-50, 50),
            ease: 'out'
          },
          { to: utils.random(-100, 100), ease: 'inOut(2)' }
        ],
        translateY: [
          {
            to: utils.random(-200, 0),
            ease: 'out'
          },
          { to: utils.random(height, height + 50), ease: 'inOut(2)' }
        ],
        duration: 1000 * (1 - scale) + 1000,
        rotate: `${utils.random(1, 3)}turn`,
        rotateX: `${utils.random(1, 3)}turn`,
        rotateY: `1turn`,
        scale: [1, 1.2, 1, 0.3],
        opacity: [1, 1, 1, 0],
        ease: 'inOut(2)'
      }).then(() => {
        // 动画执行完成后移除标签
        $animeBox.removeChild($el)
      })
    }
  }

微信小程序如何实现彩带散落效果

当然因为小程序不支持dom操作,所以不能直接用以上方式进行实现,如果你能接受以下问题的话(😂作者劝退,能不在小程序上花拳绣腿就不要作死了):

  1. animejs不支持在微信小程序上使用,尝试npm,js引入均会报错!!!

  2. 微信小程序上ios不兼容 transform-style: 'preserve-3d'效果,如下图所示(左-开发工具,安卓彩带飘落正常)(右-ios飘落无3D效果):

image.png

如果以上都还没劝退你,想必你很有执念,祝兄台一路平安🙇‍~

下面开始骚操作,没用animejs也能大搞特搞!!

实现原理

  1. 正常通过wx:for创建多个标签
  2. 优先定义好彩带.dot的css样式属性,先把公用的一些属性给定义好,再实现定义好动画样式,后期通过随机定义的dotMove动画,来匹配不同的dotMove1,dotMove2... 动画效果
.dot {
  animation-name: dotMove;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  pointer-events: none;
  opacity: 0.9;
}
@keyframes dotMove {
  to {
    transform: translate3d(-50px, 300px, 300px) rotate3d(1, 1, 1, 360deg);
  }
}
@keyframes dotMove1 {
  to {
    transform: translate3d(50px, 300px, 300px) rotate3d(1, 1, 1, 720deg);
  }
}
@keyframes dotMove2 {
  to {
    transform: translate3d(100px, 300px, 300px) rotate3d(1, 1, 1, 360deg);
  }
}
@keyframes dotMove3 {
  to {
    transform: translate3d(-100px, 300px, 300px) rotate3d(1, 1, 1, 720deg);
  }
}

  1. 创建时通过定义动画属性animation-delayanimation-name等,方便在view标签上给style属性赋值;

image.png

  1. 标签通过style结合不同的left,top,animation-name,animation-delay...来达到随机的动画效果

实现代码

<view class="container">
 <view class="dot" wx:for="{{bgFlowerArr}}" style="{{item.styleStr}}"></view>
</view>
  data: {
    bgFlowerArr: [] //礼花集合数组
  },
  ...
   // 创建礼花动画
    playAnimation() {
      const _this = this
      // 通过小程序提供的api,获取.container容器的宽高
      const query = wx.createSelectorQuery();
      query.select('.container').boundingClientRect();
      query.exec((res) => {
        const bounds = res[0]
        const bgFlowerSpeed = 0.7 // 背景礼花飘落速度
        const bgFlowerNum = 88 // 背景礼花数量
        const bgFlowerSize = 8 // 背景礼花大小
        const {
          width,
          height
        } = bounds
        let arr = []
        for (let i = 0; i < bgFlowerNum; i++) {
          const scale = utils.random(0.2, 1.4, 1)
          // 定义随机样式
          const style = {
            width: `${bgFlowerSize * scale}px`,
            height: `${bgFlowerSize * scale}px`,
            position: 'absolute',
            left: `${utils.random(0, width)}px`,
            top: `${utils.random(-100, -10)}px`,
            background: utils.randomPick([
              '#dce90d',
              '#0de9be',
              '#0dbce9',
              '#e9530d',
              '#e90d59',
              '#ffffff'
            ]),
            transform: `translate3d(0px, 0px, 0px) rotate3d(1, 1, 1, 0deg)`,
            // 因为这里不能用animejs的属性,所以直接用原生属性
            'animation-delay': utils.randomPick([-4000, -3000, -2000, -1000, 1000, 2000]) / bgFlowerSpeed,
            'animation-name': utils.randomPick(['dotMove', 'dotMove1', 'dotMove2', 'dotMove3']),
            'animation-duration': `${(6000 - 2000 * scale) / bgFlowerSpeed}ms`
          }
          // 将style对象属性转化为字符串
          const styleStr = this.styleToStr(style)
          arr.push({
            styleStr
          })
        }
        _this.setData({
          bgFlowerArr: arr
        })
      })
    }
    
    styleToStr(style) {
      return Object.keys(style).reduce((acc, cur) => {
        let value = `${cur}:${style[cur]};`
        if (cur === 'animation-delay') {
          value = `${cur}:${style[cur]}ms;`
        }
        acc += value
        return acc
      }, '')
    },
.dot {
  animation-name: dotMove;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  pointer-events: none;
  opacity: 0.9;
}
@keyframes dotMove {
  to {
    transform: translate3d(-50px, 300px, 300px) rotate3d(1, 1, 1, 360deg);
  }
}
@keyframes dotMove1 {
  to {
    transform: translate3d(50px, 300px, 300px) rotate3d(1, 1, 1, 720deg);
  }
}
@keyframes dotMove2 {
  to {
    transform: translate3d(100px, 300px, 300px) rotate3d(1, 1, 1, 360deg);
  }
}
@keyframes dotMove3 {
  to {
    transform: translate3d(-100px, 300px, 300px) rotate3d(1, 1, 1, 720deg);
  }
}

基于animejs分离出来的utils工具方法

/**
 * 从数组中随机选择一个元素
 * @param {Array} array - 源数组
 * @returns {*} 数组中的随机元素
 */
export const randomPick = (array) => {
  if (!Array.isArray(array) || array.length === 0) {
    return undefined;
  }
  
  const randomIndex = Math.floor(Math.random() * array.length);
  return array[randomIndex];
}
/**
 * 生成指定范围内的随机数
 * @param {number} min - 最小值
 * @param {number} max - 最大值
 * @param {number} [round] - 可选,保留的小数位数
 * @returns {number} 范围内的随机数
 */
export const random = (min, max, round) => {
  if (min >= max) {
    throw new Error('Min value must be less than max value');
  }
  
  const value = Math.random() * (max - min) + min;
  
  if (round !== undefined) {
    const factor = Math.pow(10, round);
    return Math.round(value * factor) / factor;
  }
  
  return value;
}