跟着掘金一起让头像旋转起来!!!!

422 阅读3分钟

一. 前言

之前在掘金主页上看到一个很好玩的动态效果,把鼠标hover到头像上,头像会旋转起来然后越来越快!!。效果如下:

20241206144312.gif

挺好玩的,那我们今天就来尝试用几种方式实现一下。

二. 实现流程

2.1 基本搭建

第一步,我们先让图片旋转起来。先新建一个html文件,简单放一个图片写一点样式。

  <img
    id="image"
    src="https://p6-passport.byteacctimg.com/img/user-avatar/c1f4615793baed479203e660f1f2a7ff~150x150.awebp"
    alt="Rotating Image"
    class="avatar-img"
  />

然后把样式写一下,如下:

.avatar-img {
  border-radius: 50%;
  width: 100px;
  height: 100px;
  -o-object-fit: cover;
  object-fit: cover;
  background-color: white;
}

@keyframes rotate-360 {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.avatar-img:hover {
  animation: rotate-360 2s ease-in-out infinite;
}

通过keyframesanimation实现让图片旋转起来。结果大概就是下面这样:

20241206144949.gif

现在可以看到,头像已经转起来了,下面就要考虑一下如何实现越转越快的效果。

2.2 减少动画持续时间

这个方案的想法是这样:

  • 鼠标hover上图片的时候,开始转圈。(就是上面的实现流程)
  • 监听一下animation事件,当转完一圈之后开始,减少animation动画的执行时间time,然后到一个临界值之后time保持不变。
  • 当鼠标移出图片的时候,时间恢复到初始值。

OK,我们来实现一下。首先要考虑的是,如何监听animation事件,就是在什么时候去监听事件。 不难理解,当一张图片旋转360度,回到开始位置的时候,运行轨迹正好是一个keyframes,那我们就可以在这最后一帧时监听事件触发

查看MDN发现animationiteration这个事件,正好就是我们想要的。

image.png

OK,开干!

先定义一个时间对象,里面包含动画执行时间每次动画减少的时间动画最终执行时间

  const TIME = {
    initTime: 2000,   // 动画执行时间
    descr: 200,  // 每次动画减少的时间
    endTime: 500  // 动画最终执行时间
  }

然后通过,监听animationiteration这个事件,在每次最后一帧的时候,减少动画执行的时间,直到时间等于 endTime。代码如下:

  const rotatingImage = document.getElementById('image');
  rotatingImage.addEventListener('animationiteration', e => {
  // 如果动画执行时间 >  动画最终执行时间, 递减
    if (initTime > endTime) {
      initTime -= descr;
      // 重新设置动画的持续时间
      rotatingImage.style.animationDuration = `${initTime}ms`;
    }
  })

我们看一下最终效果:

20241206152422.gif

虽然,实现了,但是有很多问题。

  1. 动画不连续,很卡顿。
  2. 会出现失帧的情况。

所以这种方案不是最优的,不予采用。

2.2 定时器方案

不从转的圈数上考虑的话,我们是不是可以通过setInterval来持续的减少动画执行的时间?直到这个时间到达一个临界值。这样的话,我们就不用去监听animationiteration,而是应该在鼠标移入当前元素的时候触发定时器的定时事件

我们对上面代码进行改造,监听一下鼠标移入事件mouseenter

  let interval = 0;
  // 鼠标移入事件
  rotatingImage.addEventListener('mouseenter', () => {
    interval = setInterval(() => {
      if (TIME.initTime > TIME.endTime) {
          TIME.initTime -= TIME.descr; // 每次减少200毫秒
          rotatingImage.style.animationDuration = `${TIME.initTime}ms`;
      } else {
          clearInterval(interval); // 当达到最小时长时停止改变
          interval = 0;
          TIME.initTime = 2000;
      }
    }, 200); // 每100毫秒改变一次动画时长
  });

同时,当鼠标移出的时候,我们监听一下移出事件,恢复成初始值。

  rotatingImage.addEventListener('mouseout', () => {
  if (!interval) return;
    clearInterval(interval); // 当鼠标移出时停止改变
    interval = 0;
    initTime = 2000;
    rotatingImage.style.animationDuration = `2000ms`;
  });

我们看一下效果。

20241206154254-convert.gif

感觉上好一点,但是还是会出现卡顿,以及失帧的情况。

2.3 requestAnimationFrame

既然已经想到了持续改变时间,那为什么不用requestAnimationFrame呢?他的优点就不多说了,反正用在动画渲染上是很好用的。

让我们在修改一下js代码:

// 修改一个开始值
const TIME = {
    initTime: 2000,
    descr: 10,
    endTime: 200
}
// 鼠标移入事件
rotatingImage.addEventListener('mouseenter', () => {
  function animate() {
    if (TIME.initTime > TIME.endTime) {
      TIME.initTime -= TIME.descr; // 每次减少10毫秒
        rotatingImage.style.animationDuration = `${TIME.initTime}ms`;
        // 调用requestAnimationFrame
        window.requestAnimationFrame(animate);
    }
  }
  animate();
});

20241206155127.gif 我们看一下结果:

可以看出卡顿明显好很多,但是会出现转速问题,以及速度过快问题(这个可以通过descr时间来解决),总得来说效果要好一些。

2.4 旋转角度

上面的几种方案都是从改变动画的时间入手,那我们是不是可以考虑一下从速度上入手呢??

如果我们保证,在相同的时间,逐步增加旋转的角度(其实就是在相同的时间内,半径扫过的圆的面积逐步增加),这样是不是能在视觉上实现,旋转越来越快的效果呢?答案是肯定的。

那么如何保证时间相同?也就是持续执行的时间相同?肯定是用requestAnimationFrame

实现的原理就是:

    V=V₁+at
  1. V 就是速度,也就是旋转的角度,每次requestAnimationFrame,执行完之后,都把角度重新计算。
  2. V1 累加的结果。
  3. at 时间这里默认就是1,a可以每次增加,实现速度越来越快的效果。

ok,我们实现一下。这里就不用keyframes,我们重新写一下。

  <img
    id="image"
    src="https://p6-passport.byteacctimg.com/img/user-avatar/c1f4615793baed479203e660f1f2a7ff~150x150.awebp"
    alt="Rotating Image"
    class="avatar-img"
  />
<style>
.avatar-img {
  border-radius: 50%;
  width: 100px;
  height: 100px;
  -o-object-fit: cover;
  object-fit: cover;
  background-color: white;
}

.avatar-img:hover {
  transition: transform 0.1s ease-in-out;
}
</style>

js代码如下:

const rotatingImage = document.getElementById('image');

  const trans = {
    angle: 0,   // 开始角度
    speed: 1,   // 速度开始
    increment: 0.2,   // 加速度
  }

  let animationID = null;   // 关键帧ID

  rotatingImage.addEventListener('mouseenter', () => {
    function rotateImage() {
      trans.angle += trans.speed;   // 把angle作为rotate的角度
      rotatingImage.style.transform = `rotate(${trans.angle}deg)`;   // 重新设置
          // speed的临近值
          if (trans.speed < 20) {
          // 对速度进行增加
            trans.speed += trans.increment;
          }
          animationID = requestAnimationFrame(rotateImage);
      }
    rotateImage(); 
  });

  rotatingImage.addEventListener('mouseleave', () => {
    // 初始化
    trans.speed = 1;
    trans.angle = 0;
    rotatingImage.style.transform = `rotate(0deg)`;
    // 清楚执行的动画
    if (animationID) {
        cancelAnimationFrame(animationID);
        animationID = null;
    }
  });

最终结果如下:

20241206161641.gif

大家可以试一下,效果是目前几种方法里面最好的。

三. 最后

方案肯定不止这几种,只是通过css + js,或者是js的方案实现一下,当前纯css应该也可以,但是我不会!