一. 前言
之前在掘金主页上看到一个很好玩的动态效果,把鼠标hover
到头像上,头像会旋转起来然后越来越快!!
。效果如下:
挺好玩的,那我们今天就来尝试用几种方式实现一下。
二. 实现流程
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;
}
通过keyframes
和animation
实现让图片旋转起来。结果大概就是下面这样:
现在可以看到,头像已经转起来了,下面就要考虑一下如何实现越转越快的效果。
2.2 减少动画持续时间
这个方案的想法是这样:
- 鼠标
hover
上图片的时候,开始转圈。(就是上面的实现流程) - 监听一下
animation
事件,当转完一圈之后开始,减少animation
动画的执行时间time,然后到一个临界值之后time保持不变。 - 当鼠标
移出
图片的时候,时间恢复到初始值。
OK,我们来实现一下。首先要考虑的是,如何监听animation
事件,就是在什么时候去监听事件。
不难理解,当一张图片旋转360度,回到开始位置的时候,运行轨迹正好是一个keyframes
,那我们就可以在这最后一帧时监听事件触发
。
查看MDN
发现animationiteration
这个事件,正好就是我们想要的。
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`;
}
})
我们看一下最终效果:
虽然,实现了,但是有很多问题。
- 动画不连续,很卡顿。
- 会出现失帧的情况。
所以这种方案不是最优的,不予采用。
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`;
});
我们看一下效果。
感觉上好一点,但是还是会出现卡顿,以及失帧的情况。
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();
});
我们看一下结果:
可以看出卡顿明显好很多,但是会出现转速问题,以及速度过快问题(这个可以通过descr时间来解决),总得来说效果要好一些。
2.4 旋转角度
上面的几种方案都是从改变动画的时间入手,那我们是不是可以考虑一下从速度
上入手呢??
如果我们保证,在相同的时间
,逐步增加旋转的角度
(其实就是在相同的时间内,半径扫过的圆的面积逐步增加),这样是不是能在视觉上实现,旋转越来越快的效果呢?答案是肯定的。
那么如何保证时间相同?也就是持续执行的时间相同?肯定是用requestAnimationFrame
。
实现的原理就是:
V=V₁+at
V
就是速度,也就是旋转的角度,每次requestAnimationFrame
,执行完之后,都把角度重新计算。V1
累加的结果。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;
}
});
最终结果如下:
大家可以试一下,效果是目前几种方法里面最好的。
三. 最后
方案肯定不止这几种,只是通过css + js
,或者是js
的方案实现一下,当前纯css
应该也可以,但是我不会!