有段时间没更文了,起初动画库选的 GSAP,奈何后面发现英文文档实在难以下咽,而且官方案例又很少,学习难度增大。
GSAP和Animejs实际用法上大差不差,Animejs属于后起之秀吧,写法上可能稍微比前者有所优化,所以我不会太惧怕🙄
20年刚入行开始学动画的时候也是选的 Animejs,可能当时还没崛起。很多功能并没有GSAP那么全面,不过现在看起来。Animejs才是真正的神,这几年社区发展的很快,很多功能也都赶上了,更重要的是有 中文文档,案例也都很完善,建议大家少走弯路,选它!选它!选它!
背景
前端现阶段也有很多组件库,但很多时候如果想要自定义动画效果,例如轮播图,导航栏,拖拽等效果就有点费劲了。
如果大家对这些没有过多要求的话,那你可以忽略这篇文章。因为下面只是为了单纯的让你了解anime一些特性,让你可以做出自定义的动画效果,实际应用上和市面上的组件库差不太多。
效果展示
以上是实机演示效果,其中包括3点:
- 头部导航点击定位
- 内容部分可拖拽定位
- 底部上一页,下一页点击定位
如果你对这个效果很感兴趣,那么请继续往下看,会针对以上3点进行功能拆解,当然你也可以直接到文章底部看 👉在线代码源码😎。
实现步骤
引入anime的方式很简单,你也可以直接参考 👉官方文档
引入animejs
npm install animejs
import { animate } from 'animejs';
创建拖拽容器
引入anime的 Draggable 插件
import { createDraggable } from 'animejs';
div id="bounded-flick" class="flicker container margin">
<ul class="carousel">
<li class="draggable carousel-item">1</li>
<li class="draggable carousel-item">2</li>
<li class="draggable carousel-item">3</li>
<li class="draggable carousel-item">4</li>
</ul>
</div>
const boundedFlickWidth = 280 + 10; // 定义每个轮播项的宽度(280px内容 + 10px间距)
// 获取轮播项的数量(计算轮播内容的总宽度需要)
const boundedFlickLength = utils.$("#bounded-flick .carousel-item").length;
// 设置轮播容器的总宽度(所有轮播项宽度之和)
utils.set("#bounded-flick .carousel", {
width: `${boundedFlickLength * boundedFlickWidth}`
});
// -10px间距,280px为每个item的真实宽高
utils.set([".container", ".carousel-item"], {
width: boundedFlickWidth - 10,
height: boundedFlickWidth - 10
});
// 创建可拖拽的轮播容器
const boundedFlicker = createDraggable("#bounded-flick .carousel", {
// Array<Number> ([top, right, bottom, left]) 设置X轴可拖动范围
container: [0, 0, 0, -boundedFlickWidth * (boundedFlickLength - 1)],
y: false, // 禁止垂直拖动
/**
* 将两个轴或一个特定轴的最终值四舍五入到最接近的指定增量。
* 如果提供一个 Array 作为增量,它将从数组中选择最接近的值
*/
snap: boundedFlickWidth, // 拖动时以290px为单位吸附(实现分页效果)
// 更新时,每次拖拽更新时就会执行此方法
onUpdate: (e) => {
...
}
});
创建头部nav导航
引入anime的 Utils 插件
import { utils } from 'animejs';
<ul class="navigation">
<li class="nav-item">1</li>
<li class="nav-item">2</li>
<li class="nav-item">3</li>
<li class="nav-item">4</li>
</ul>
const boundedFlicker = createDraggable("#bounded-flick .carousel", {
...
// 更新时,每次拖拽更新时就会执行此方法
onUpdate: (e) => {
// 牵涉到 activeIndex 下标位置,因此在每次更新时需要获取最新得下标,以及相关样式更新
getActiveIndex(e.x);
}
});
// 创建nav导航点击事件 index:当前下标 isInit:是否为初始化
const clickNavItem = (index, isInit = false) => {
// 根据坐标计算当前位置下标,同步拖拽容器得动画执行位置
animate(boundedFlicker, {
x: -index * boundedFlickWidth, //x轴偏移位置
duration: isInit ? 0 : 500,
ease: "out(4)"
});
};
...
// 初始化下标位置
let activeIndex = 2;
clickNavItem(activeIndex, true);
// 获取当前位置下标
const getActiveIndex = (x) => {
const index = utils.round(x / -boundedFlickWidth, 0);
activeIndex = index;
// updateActiveIndexClass({ index, x }); //此处是用于更新相关样式,不影响正常运行
};
// 通过utils工具获取nav元素,并添加点击事件监听
const navs = utils.$(".nav-item");
navs.forEach(($el, index) => {
$el.addEventListener("click", () => {
clickNavItem(index);
});
});
创建底部前后切换定位
<div class="controls">
<button class="control-button prve">Prev</button>
<button class="control-button next">Next</button>
</div>
// 创建底部导航控制方法
const slideTo = (targetIndex) => {
// 控制切换时最大最小下标
const clampedIndex = Math.max(0,Math.min(targetIndex, boundedFlickLength - 1));
// 根据坐标计算当前位置下标,同步拖拽容器得动画执行位置
animate(boundedFlicker, {
x: -clampedIndex * boundedFlickWidth,
duration: 500,
ease: "out(4)"
});
};
...
// 通过utils工具获取切换元素,并添加点击事件监听
const [$prev, $next] = utils.$(".control-button");
$prev.addEventListener("click", () => slideTo(activeIndex - 1));
$next.addEventListener("click", () => slideTo(activeIndex + 1));
大功告成🎉
了解以上拆分得步骤后,基本可以满足需求了。
动画提升
以下纯纯动画提神醒脑,想多了解的可以看看,实际功能是已经实现的了。
.seek()时间轴关联
这里值得一提的是anime的 .seek() 更新动画的 currentTime 并将其推进到特定时间
应用到当前项目中,就是在拖拽时更新nav导航底部进度条.plan的样式,类似下面这样:
<div class="plan"></div>
// 创建进度条动画
utils.set(".plan", { width: 0, transformOrigin: "left" });
const planAnimation = animate(".plan", {
autoplay: false,
width: "100%",
ease: "linear" //如果想要等比动画,这里一定要设置匀速!!!!
});
...
// 根据当前下标移除并添加样式
const updateActiveIndexClass = ({ index, x }) => {
...
// 根据当前拖拽距离同步进度条动画的播放时间,默认动画时间为1000ms
planAnimation.seek(
(x / (-boundedFlickWidth * boundedFlickLength)) * 1000 + 1000 / boundedFlickLength
); // 每个carousel-item偏移时所需要的平均时间 + 补偿时间(拖拽位置x为0时,实际的头部进度条应该到达1的位置,所以需要 1000/4=250ms 的补偿)
};
遇到的问题
- 这里你可能想到用
scaleX属性,但是实际应用却不生效🕵️♀️(我没做过多深究所以就转用width控制宽度了); - 为何要重新在js里控制下
width:0,这个官方也有解释,大致意思就是anime不会解析css中的样式,如果想要变换尽量.set()下才能监听到
3.ease: "linear"如果想要同步关联进度条动画,这里切记要用匀速,否则拖拽时头部的.plan样式无法同步;
在线代码
以上为在线代码,需要在vue3中引用的,可以👉参考这里。
希望有帮到你,愿将来你能成为一个anime动画小能手💪,敬us!