动画类型
序列帧(逐帧)动画
是通过顺序播放一系列连续的静态图像,由人眼的视觉暂留效应来产生动画效果,如常见的GIF格式动画。
这种方式简单粗暴,便于精细控制,适合用来表现复杂的动作或细节,常用于2D动画、游戏角色动作等。但由于每一帧都需要单独绘制或生成,导致制作成本加大,最终生成的文件体积也较大。
补间(关键帧)动画
关键帧动画只需要定义动画中的重要帧(关键帧),中间帧由算法自动生成。
前端动画的技术实现
CSS transition 和animation 属性是最简单便捷的实现关键帧动画的方式。
CSS Transitions
css中的transition 属性可以为一个元素定义在不同状态之间切换时的过渡效果。比如在不同的伪类之间切换,像是 :hover,:active 或者通过 JavaScript 实现的状态变化。
button {
background-color: #3d3d3d;
transition: background-color .5s ease-in;
}
button:hover {
background-color: #252627;
}
CSS Animations
transition只能定义初始和终止状态之间的过渡效果,而animation 可以通过@keyframes 规则定义多个状态帧,从而实现更为精细、复杂的过渡动画效果。
@keyframes move {
from {
transform: translate(0, 0);
}
50% {
transform: translate(150px, 100px);
}
to {
transform: translate(10px, 10px);
}
}
div {
animation: move 2s ease-in-out infinite;
animation-play-state: running; /* paused / running */
}
还可同时定义多个不同的动画效果:
@keyframes fade {
from {
opacity: 1;
}
50% {
opacity: .25;
}
to {
opacity: 1;
}
}
div {
animation:
move 2s ease-in-out infinite,
fade 1s ease-out infinite;
}
JS + DOM
使用js控制DOM样式是一种比较传统的动画实现方式,这种方式灵活性高、兼容性好,而且既可以实现逐帧动画,也可以实现关键帧动画。
- js+雪碧图实现逐帧动画
雪碧图(sprite, 也叫精灵图)技术是将多个小图标合成到一张大图里,然后通过css中的background-position属性来控制背景剪切的区域。因此可以利用这种方式,将动画帧制作成雪碧图,再用js动态修改background-position属性值来实现逐帧动画。
举例说明,如雪碧图中的帧按照从左到右、从上到下的顺序排列,每一帧的尺寸为WxH
/**
* 使用 background-position 和 requestAnimationFrame 实现逐帧动画
* @param {HTMLElement} element - 需要动画的元素
* @param {string} imageUrl - 背景图片的 URL
* @param {number} frameWidth - 每一帧的宽度
* @param {number} frameHeight - 每一帧的高度
* @param {number} totalFrames - 总帧数
* @param {number} loopCount - 循环次数(Infinity 表示无限循环)
*/
function createFrameAnimation(element, imageUrl, frameWidth, frameHeight, totalFrames, loopCount = Infinity) {
let currentFrame = 0; // 当前帧索引
let currentLoop = 0; // 当前循环次数
let isPlaying = true; // 是否正在播放
let animationId = null; // requestAnimationFrame 的 ID
// 设置背景图片和初始样式
element.style.backgroundImage = `url(${imageUrl})`;
element.style.backgroundRepeat = 'no-repeat';
element.style.width = `${frameWidth}px`;
element.style.height = `${frameHeight}px`;
// 更新 background-position 的函数
function updateFrame() {
if (!isPlaying) return;
// 计算当前帧的 background-position
const xOffset = -currentFrame * frameWidth;
element.style.backgroundPosition = `${xOffset}px 0`;
// 更新帧索引
currentFrame = (currentFrame + 1) % totalFrames;
// 如果当前帧回到 0,表示完成一次循环
if (currentFrame === 0) {
currentLoop++;
if (currentLoop >= loopCount) {
stopAnimation();
return;
}
}
// 继续下一帧
animationId = requestAnimationFrame(updateFrame);
}
// 开始动画
function startAnimation() {
if (isPlaying) return;
isPlaying = true;
animationId = requestAnimationFrame(updateFrame);
}
// 停止动画
function stopAnimation() {
isPlaying = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 重置动画
function resetAnimation() {
currentFrame = 0;
currentLoop = 0;
element.style.backgroundPosition = '0 0';
}
// 初始化
startAnimation();
// 返回控制方法
return {
start: startAnimation,
stop: stopAnimation,
reset: resetAnimation,
};
}
也可以通过transform: translate3d() 直接移动动画元素并开启GPU加速,不过动画元素需要再套一层wrapper元素,并为其设置overflow: hidden; ,原理相同,不再赘述。
通过js动态修改DOM元素的css属性实现逐帧动画
通过js动态修改DOM元素的css属性值能实现更加精细、复杂的动效控制,如改变元素的位置、颜色和透明度等。通过js实现DOM关键帧动画
js也可以实现关键帧动画:预先定义好关键帧的状态,然后在关键帧之间通过js计算中间状态并更新DOM样式。
步骤如下:- 定义关键帧
- 计算插值
- 更新DOM
代码实现
/**
* 关键帧动画类
* @param {HTMLElement} element - 需要动画的元素
* @param {Array} keyframes - 关键帧数组,格式为 [{ time: 0, styles: { ... } }, ...]
* @param {number} duration - 动画总时长(毫秒)
*/
class KeyframeAnimation {
constructor(element, keyframes, duration) {
this.element = element;
this.keyframes = keyframes.sort((a, b) => a.time - b.time); // 按时间排序
this.duration = duration;
this.startTime = null;
this.isPlaying = false;
this.animationId = null;
}
// 开始动画
start() {
if (this.isPlaying) return;
this.isPlaying = true;
this.startTime = performance.now();
this.animate();
}
// 停止动画
stop() {
this.isPlaying = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
// 重置动画
reset() {
this.stop();
this.applyStyles(this.keyframes[0].styles); // 重置到第一帧
}
// 动画循环
animate() {
if (!this.isPlaying) return;
const currentTime = performance.now() - this.startTime;
const progress = Math.min(currentTime / this.duration, 1); // 计算进度(0 到 1)
// 应用当前帧的样式
this.applyFrame(progress);
// 如果动画未结束,继续下一帧
if (progress < 1) {
this.animationId = requestAnimationFrame(() => this.animate());
} else {
this.stop(); // 动画结束
}
}
// 应用当前帧的样式
applyFrame(progress) {
const frame = this.getCurrentFrame(progress);
this.applyStyles(frame.styles);
}
// 获取当前帧的样式
getCurrentFrame(progress) {
const time = progress * this.duration; // 当前时间
let startFrame, endFrame;
// 找到当前时间所在的关键帧区间
for (let i = 0; i < this.keyframes.length - 1; i++) {
if (time >= this.keyframes[i].time && time <= this.keyframes[i + 1].time) {
startFrame = this.keyframes[i];
endFrame = this.keyframes[i + 1];
break;
}
}
// 如果没有找到区间,返回最后一帧
if (!startFrame || !endFrame) {
return this.keyframes[this.keyframes.length - 1];
}
// 计算插值
const frameProgress = (time - startFrame.time) / (endFrame.time - startFrame.time);
const interpolatedStyles = this.interpolateStyles(startFrame.styles, endFrame.styles, frameProgress);
return { styles: interpolatedStyles };
}
// 插值计算
interpolateStyles(startStyles, endStyles, progress) {
const styles = {};
for (const key in endStyles) {
if (typeof startStyles[key] === 'number' && typeof endStyles[key] === 'number') {
styles[key] = startStyles[key] + (endStyles[key] - startStyles[key]) * progress; // 线性插值
} else {
styles[key] = endStyles[key]; // 非数字属性直接使用结束值
}
}
return styles;
}
// 应用样式到 DOM 元素
applyStyles(styles) {
for (const key in styles) {
this.element.style[key] = styles[key];
}
}
}
使用方法
const box = document.getElementById('box');
const keyframes = [
{ time: 0, styles: { transform: 'translateX(0px)', opacity: 1 } },
{ time: 500, styles: { transform: 'translateX(200px)', opacity: 0.5 } },
{ time: 1000, styles: { transform: 'translateX(400px)', opacity: 1 } },
];
const animation = new KeyframeAnimation(box, keyframes, 1000); // 1000ms 动画
animation.start(); // 开始动画
// animation.stop(); // 停止动画
// animation.reset(); // 重置动画
此外,还有许多第三方库可以实现关键帧动画,如anime.js 等。
利用animation steps()实现逐帧动画
animation-timing-function 属性设置动画在每个周期的持续时间内如何进行。它的值除了常见的几个缓动函数的关键字(linear ease ease-in 等)和三次贝赛尔曲线cubic-bezier() 函数外,还可设置为steps(n) 阶跃函数,它表示动画在相邻两个关键帧之间按照n 个定格发生跳跃,具体可参考MDN文档 。
因此,我们可以利用这个特性,实现帧跳跃:
.sprite {
width: 100px;
height: 100px;
background-repeat: no-repeat;
background-image: url(frames.png);
animation: move 100ms steps(1, end) both infinite;
}
@keyframes move {
from {background-position: 0 0;}
5% {background-position: -100px 0;}
10% {background-position: -200px 0;}
...
to {background-position: -1000px 0;}
}
如不需要精确控制中间状态,也可简化成
.sprite {
...
animation: move 2s steps(20) both infinite;
}
@keyframes move {
from {background-position: 0 0;}
to {background-position: -1000px 0;}
}
表示将0%~100%这个区间分成20步来阶段性展示。
animation-delay和css变量实现动画
animation-delay 属性可以设置animation 动画的延迟时间,当值为负数时,它表示动画从初始状态之前的某个时刻开始运行。利用这种机制,我们将animation-delay 属性设为负值,同时,将animation-play-state 属性设置为paused ,此时,动画将保持静止在过去的某一时刻的状态。我们只需要利用css变量动态地修改animation-delay 的值,就能实现动画的效果。
这种方式的好处是它能有效利用animation 的自动插补算法实现复杂动画,同时方便地控制动画的运行状态,因此非常便于实现一些与用户交互动作有关的复杂动画。
div {
width: 0;
height: 10px;
background-color: red;
animation: grow 2s infinite;
animation-play-state: paused;
animation-delay: var(--delay);
}
@keyframes grow {
to {
width: 100%;
background-color: yellow;
}
}
const div = document.querySelectorAll('div')[0]
let t = 0
requestAnimationFrame(function grow() {
t = (t + 0.01) % 2;
div.style.setProperty('--delay', `-${t}s`);
requestAnimationFrame(grow);
})
SVG
路径动画