7个div完成翻牌小动画

2,631 阅读4分钟

FLIP

说起FLIP动画, 首次接触它是在vue官方文档中讲过渡动画的时候, 一开始只是觉得可能只是一个前端的动画库, 类似aimate.js, 实则不然, FLIP只是以下单词的首字母

  • First(元素初始的位置)
  • List(元素动画结束后的位置)
  • Invert(元素初始位置至结束位置发生的转化, 可以参考transform的属性)
  • Play(动画的启动停止)

由此可知, FLIP只是一个概念, 我们平时运用过渡或者animate动画的时候可能不知不觉可能已经运用到了. 关于概念不赘述了不想水太多

制作动画

首先创建一个容器, 里面有六张卡片, 其中wrap设置成flex布局, 水平垂直居中, 且被类wrap包裹的6个div设置成绝对定位且设置不同层级, 让它们汇聚在中心, 因为有六张卡片首先聚在一起旋转, 所以我们每张卡片的角度错开60度(360/6)

  <div class="wrap">
     <div class="card1 active"></div>
     <div class="card2 active"></div>
     <div class="card3 active"></div>
     <div class="card4 active"></div>
     <div class="card5 active"></div>
     <div class="card6 active"></div>
  </div>
@keyframes xz1 {
    from {
       transform: rotateZ(0deg);
    }
    to {
       transform: rotateZ(360deg);
     }
 }
 
.card1 {
        position: absolute;
        z-index: 1;
        animation: xz1 2s linear infinite;
      }

......

这时候六张牌重叠着旋转了起来, 接下来使用dom对象上的animate方法, 其实可以写👆的代码, 因为之前不知道dom上还会有这个animate方法,所以想尝试一下

  const arr = document.querySelectorAll('.wrap > div')
  const [dom1, dom2, dom3, dom4, dom5, dom6] = arr
  dom1.animate([
     { transform: 'translateY(0) translateX(0)' },    
     { transform: `translateY(115px) translateX(180)px)` },
  ],
  {
     duration: 300,
     easing: 'cubic-bezier(0,0,0.32,1)',
  })

animate函数中第一个参数参考css中@keyframe, 第二个参数类似于css中animate中属性(duration, easeing, direction, delay, fill, iteration), Element.animate()传送门

等洗牌结束, 六张牌会按顺序排列成两行, 这里六张牌我用的是绝对定位, 因为这六张牌需要重叠在一起洗牌, 需要设置层级, 二来洗牌之后回归到自己的位置, 不设置层级的话div会发生挤压, 我这里因为牌数比较少所以写死了位置, 而且六张牌飞到六个位置的顺序也是固定的, 因为随机飞到位置的动画感觉不是很平滑, 假如想纸牌随机飞入一个六个位置中的一个可以把定义位置的数组顺序打乱一下, 类似于这样

const positionArr = [
          positionFunc(115, 180),
          positionFunc(115, 10),
          positionFunc(115, -160),
          positionFunc(-75, 180),
          positionFunc(-75, 10),
          positionFunc(-75, -160),
        ].sort(() => Math.random() - 0.5 )

当停止洗牌的时候, 直接暂停旋转动画然后切换样式, 切换后的样式只是和原来去掉了animate和增加了定位


// 旋转动画
.card1 {
   position: absolute;
   z-index: 1;
   animation: xz1 2s linear infinite;
}

// 旋转动画结束后
.card11 {
   position: absolute;
   z-index: 1;
   left: 0;
   top: 0;
}

在洗牌的中心点分散到6个位置时也伴随着动画, 此时我们还是用animate()加入移动的动画

const commonConfig = {
        duration: 300,
        easing: 'cubic-bezier(0,0,0.32,1)',
      }
      
const positionArr = [
     positionFunc(-115, -180),
     positionFunc(-115, -10),
     positionFunc(-115, 160),
     positionFunc(75, -180),
     positionFunc(75, -10),
     positionFunc(75, 160),
   ]
   
const player1 = dom1.animate(positionArr[0], commonConfig)

最后是翻牌阶段, 自然而然会想到使用transform中的rotateY, 思路是在卡片旋转的中过程中替换成奖品的图片, 但是对于左右不对称的图片来说, 因为div旋转180度的缘故, 左右部分会互换, 所以要么是图片本身就是左右颠倒的,要么直接对img标签设置样式, 达到负负得正的效果

.active > img {
        width: 120px;
        height: 140px;
        transform: rotateY(-180deg);
        object-fit: cover;
      }

还有一点需要的注意的是, 动画毕竟就是一种过渡的效果, 不是无限循环的话, 动画结束终究会还原成原来的样子, 所以我们在翻牌的过程中需要保留最后一帧

const player = targetDom.animate(
            [{ transform: 'rotateY(0deg)' }, { transform: 'rotateY(180deg)' }],
            {
              duration: 800,
              easing: 'linear',
              fill: 'forwards',  // 保留动画结束前的最后一帧
            }
          )

由上面的代码可知, 翻牌需要800毫秒, 且动画速度是线性的, 所以我们在400毫秒卡片转到90度的时候替换是最好的时机, Element.animate()会返回一个Animation对象, 它只有监听取消和结束的事件, 所以我们需要其他的api时时监听它的currentTime属性

function step(timestamp) {
       if (player.currentTime < 400) {
          requestAnimationFrame(step)
       } else {
          targetDom.innerHTML = `<img src="./img/nodata.png" />`
       }
}

requestAnimationFrame(step)

传送门

最后附上代码链接 新人第一次发文, 多有不足, 请多指教