掌握 Svelte 自定义动画:从基础到高级的实现技巧
一、Svelte 动画基础概述
1.1 Svelte 动画系统简介
Svelte 拥有一套简洁且强大的动画系统,它允许开发者为应用中的元素添加动画效果,而无需依赖复杂的第三方库。这一动画系统基于声明式的语法,使得动画的创建和管理变得直观和高效。
在 Svelte 中,动画通过 animate:
前缀结合 CSS 属性来实现。例如,要为一个元素的 opacity
属性添加动画,可以这样写:
<script>
let show = true;
</script>
{#if show}
<div animate:opacity="{{ duration: 1000, delay: 500, easing: 'ease-in-out' }}">
这是一个带有动画的 div
</div>
{/if}
在上述代码中,animate:opacity
表示对 opacity
属性进行动画操作。duration
定义了动画的持续时间为 1000 毫秒,delay
表示延迟 500 毫秒后开始动画,easing
指定了动画的缓动函数为 ease - in - out
。
1.2 内置动画函数
Svelte 提供了一些内置的动画函数,这些函数可以帮助我们快速实现常见的动画效果。
- fade:淡入淡出动画。它可以控制元素的
opacity
属性,同时还可以选择是否在动画开始或结束时隐藏元素。
<script>
import { fade } from'svelte/transition';
let visible = true;
</script>
<button on:click={() => visible =!visible}>切换可见性</button>
{#if visible}
<div transition:fade="{{ duration: 1000, delay: 500 }}">
淡入淡出的内容
</div>
{/if}
- slide:滑动动画。通过改变元素的
transform
属性(通常是translateY
或translateX
)来实现元素的滑动效果。
<script>
import { slide } from'svelte/transition';
let show = false;
</script>
<button on:click={() => show =!show}>显示/隐藏</button>
{#if show}
<div transition:slide="{{ y: 100, duration: 800, easing: 'linear' }}">
滑动显示的内容
</div>
{/if}
在这个例子中,y: 100
表示元素从距离初始位置下方 100 像素的地方滑动进入,duration
为 800 毫秒,缓动函数为 linear
。
3. scale:缩放动画。通过改变元素的 transform
属性中的 scale
值来实现缩放效果。
<script>
import { scale } from'svelte/transition';
let active = false;
</script>
<button on:click={() => active =!active}>激活缩放</button>
{#if active}
<div transition:scale="{{ start: 0.5, end: 1, duration: 600 }}">
缩放的内容
</div>
{/if}
这里 start: 0.5
表示缩放起始值为 0.5(即缩小为原来的一半),end: 1
表示最终恢复到原始大小,动画持续时间为 600 毫秒。
二、自定义动画的基础实现
2.1 创建简单的自定义动画
我们可以通过 Svelte 的 transition
指令来创建自定义动画。首先,定义一个函数,该函数接受目标元素以及一些配置参数,并返回一个包含 duration
和 tick
函数的对象。tick
函数用于在动画每一帧更新元素的样式。
<script>
function myCustomTransition(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 1000);
return {
duration: params.duration || 1000,
tick: (t) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.transform = `translateX(${progress * params.distance}px)`;
}
};
}
let show = false;
</script>
<button on:click={() => show =!show}>显示动画</button>
{#if show}
<div transition:myCustomTransition="{{ distance: 200, duration: 1500 }}">
自定义平移动画的内容
</div>
{/if}
在上述代码中,myCustomTransition
函数创建了一个自定义的水平平移动画。params.distance
定义了元素最终移动的距离,params.duration
定义了动画的持续时间。tick
函数根据动画的进度计算出元素当前的 translateX
值并应用到元素上。
2.2 理解动画生命周期
在 Svelte 的自定义动画中,理解动画的生命周期非常重要。动画的生命周期包括三个阶段:进入(当元素首次出现在 DOM 中)、更新(当元素的状态或属性发生变化)和离开(当元素从 DOM 中移除)。
- 进入动画:当元素首次渲染到 DOM 中时,
transition
指令中的动画函数会被调用,进入动画开始执行。例如,我们上面定义的myCustomTransition
函数在元素首次显示时会使元素从初始位置开始水平移动。 - 更新动画:当元素的某些属性发生变化时,可以触发更新动画。要实现更新动画,我们需要在
transition
指令中使用update
选项。
<script>
function updateTransition(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 1000);
return {
duration: params.duration || 1000,
update: (t, newParams) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.transform = `scale(${progress * newParams.scaleFactor})`;
}
};
}
let scaleFactor = 1;
function increaseScale() {
scaleFactor += 0.5;
}
</script>
<button on:click={increaseScale}>增大缩放</button>
<div transition:updateTransition="{{ scaleFactor, duration: 800 }}">
更新缩放动画的内容
</div>
在这个例子中,当点击按钮时,scaleFactor
会增加,从而触发更新动画,使元素逐渐缩放。update
函数接受新的参数 newParams
,并根据动画进度更新元素的缩放样式。
3. 离开动画:当元素从 DOM 中移除时,会执行离开动画。我们可以在 transition
指令中使用 leave
选项来定义离开动画。
<script>
function leaveTransition(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 1000);
return {
duration: params.duration || 1000,
leave: (t) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.opacity = `${1 - progress}`;
}
};
}
let visible = true;
</script>
<button on:click={() => visible = false}>隐藏元素</button>
{#if visible}
<div transition:leaveTransition="{{ duration: 1000 }}">
带有离开动画的内容
</div>
{/if}
在这个例子中,当点击按钮隐藏元素时,leaveTransition
函数会使元素逐渐淡入消失,leave
函数根据动画进度更新元素的 opacity
属性。
三、高级自定义动画技巧
3.1 组合动画
在实际应用中,我们常常需要将多个动画组合在一起,以实现更复杂的效果。Svelte 允许我们通过数组的方式将多个动画函数组合起来。
<script>
import { fade } from'svelte/transition';
function slideIn(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 1000);
return {
duration: params.duration || 1000,
tick: (t) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.transform = `translateY(${progress * params.distance}px)`;
}
};
}
let show = false;
</script>
<button on:click={() => show =!show}>显示组合动画</button>
{#if show}
<div transition={[slideIn({ distance: -100, duration: 800 }), fade({ duration: 500 })]}>
组合动画的内容
</div>
{/if}
在上述代码中,slideIn
动画使元素从下方 100 像素的位置向上滑动进入,持续时间为 800 毫秒。然后,fade
动画使元素在 500 毫秒内淡入。这样就实现了一个先滑动再淡入的组合动画。
3.2 基于时间线的动画
时间线动画允许我们按照特定的顺序和时间间隔来播放多个动画。我们可以通过 svelte/motion
模块中的 animate
函数来实现基于时间线的动画。
<script>
import { animate } from'svelte/motion';
let boxStyles = { x: 0, y: 0 };
function startAnimation() {
animate(boxStyles, { x: 200, y: 0 }, { duration: 1000 })
.then(() => animate(boxStyles, { x: 200, y: 200 }, { duration: 1000 }))
.then(() => animate(boxStyles, { x: 0, y: 200 }, { duration: 1000 }))
.then(() => animate(boxStyles, { x: 0, y: 0 }, { duration: 1000 }));
}
</script>
<button on:click={startAnimation}>开始时间线动画</button>
<div style="position: relative;">
<div style="{{ position: 'absolute', left: `${boxStyles.x}px`, top: `${boxStyles.y}px`, width: '50px', height: '50px', background: 'blue' }}"></div>
</div>
在这个例子中,animate
函数用于对 boxStyles
中的 x
和 y
属性进行动画操作。通过链式调用 then
,我们可以按照顺序依次执行四个动画,使元素在一个正方形的路径上移动。
3.3 响应式动画
响应式动画是指动画能够根据应用的状态或用户交互实时调整。例如,我们可以根据窗口大小的变化来调整动画的参数。
<script>
import { derived, onMount } from'svelte/store';
import { scale } from'svelte/transition';
const windowWidth = derived({
subscribe: (set) => {
const handleResize = () => set(window.innerWidth);
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}
});
let show = false;
let scaleFactor;
windowWidth.subscribe((width) => {
if (width > 600) {
scaleFactor = 1.5;
} else {
scaleFactor = 1.2;
}
});
</script>
<button on:click={() => show =!show}>显示响应式动画</button>
{#if show}
<div transition:scale="{{ start: 1, end: scaleFactor, duration: 800 }}">
响应式缩放动画的内容
</div>
{/if}
在上述代码中,通过 derived
存储来监听窗口宽度的变化。根据窗口宽度,scaleFactor
会动态调整。当显示动画时,元素会根据当前的 scaleFactor
进行缩放,从而实现响应式动画效果。
3.4 动画性能优化
- 硬件加速:利用 CSS 的
will-change
属性来提示浏览器提前准备动画所需的资源,从而提高动画性能。例如,在自定义动画函数中,可以在动画开始前设置node.style.willChange = 'transform';
,这样浏览器会提前为元素的transform
动画做优化准备。 - 减少重排和重绘:尽量避免在动画过程中频繁改变元素的布局属性(如
width
、height
等)。如果必须改变这些属性,可以考虑使用transform
来模拟类似的效果。例如,要实现元素的放大效果,使用transform: scale(2);
比直接改变width
和height
属性更高效,因为transform
动画不会触发重排,只会触发重绘。 - 使用 requestAnimationFrame:在自定义动画的
tick
函数中,尽量使用requestAnimationFrame
来控制动画的帧率。虽然 Svelte 内部已经对动画的帧率进行了优化,但在某些复杂场景下,手动使用requestAnimationFrame
可以进一步提高动画的流畅度。例如:
<script>
function customAnimation(node, params) {
let rafId;
const start = window.performance.now();
const end = start + (params.duration || 1000);
function tick() {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.opacity = `${progress}`;
if (progress < 1) {
rafId = requestAnimationFrame(tick);
}
}
rafId = requestAnimationFrame(tick);
return {
duration: params.duration || 1000,
destroy: () => cancelAnimationFrame(rafId)
};
}
let show = false;
</script>
<button on:click={() => show =!show}>显示优化动画</button>
{#if show}
<div transition:customAnimation="{{ duration: 1000 }}">
优化后的动画内容
</div>
{/if}
在这个例子中,tick
函数使用 requestAnimationFrame
来循环更新元素的 opacity
属性,确保动画以合适的帧率运行。同时,在动画结束或组件销毁时,通过 destroy
函数取消 requestAnimationFrame
,避免内存泄漏。
四、在实际项目中应用自定义动画
4.1 导航栏动画
在一个网页应用的导航栏中,我们可以添加动画效果来提升用户体验。例如,当用户点击导航项时,导航项可以通过动画展开或收缩子菜单。
<script>
function slideDown(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 500);
const height = node.offsetHeight;
node.style.height = '0';
node.style.overflow = 'hidden';
return {
duration: params.duration || 500,
tick: (t) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.height = `${progress * height}px`;
}
};
}
let isMenuOpen = false;
function toggleMenu() {
isMenuOpen =!isMenuOpen;
}
</script>
<button on:click={toggleMenu}>切换菜单</button>
{#if isMenuOpen}
<div transition:slideDown="{{ duration: 500 }}">
<ul>
<li>菜单项 1</li>
<li>菜单项 2</li>
<li>菜单项 3</li>
</ul>
</div>
{/if}
在上述代码中,当点击按钮时,slideDown
动画使子菜单从高度为 0 逐渐展开到实际高度,给用户一种流畅的展开效果。
4.2 卡片翻转动画
在一个卡片展示的应用中,我们可以为卡片添加翻转动画,当用户点击卡片时,卡片可以翻转展示背面的内容。
<script>
function flip(node, params) {
const start = window.performance.now();
const end = start + (params.duration || 800);
return {
duration: params.duration || 800,
tick: (t) => {
const elapsed = window.performance.now() - start;
const progress = Math.min(1, elapsed / (end - start));
node.style.transform = `perspective(1000px) rotateY(${progress * 180}deg)`;
}
};
}
let isFlipped = false;
function flipCard() {
isFlipped =!isFlipped;
}
</script>
<div class="card" on:click={flipCard}>
{#if!isFlipped}
<div class="front" transition:flip="{{ duration: 800 }}">
正面内容
</div>
{:else}
<div class="back" transition:flip="{{ duration: 800 }}">
背面内容
</div>
{/if}
</div>
<style>
.card {
position: relative;
width: 200px;
height: 300px;
transform - style: preserve - 3d;
}
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface - visibility: hidden;
display: flex;
justify - content: center;
align - items: center;
}
.back {
transform: rotateY(180deg);
}
</style>
在这个例子中,通过 flip
动画函数,当点击卡片时,卡片会围绕 Y 轴旋转 180 度,实现翻转效果。同时,利用 CSS 的 perspective
和 backface - visibility
属性来创建 3D 翻转的视觉效果。
4.3 加载动画
在数据加载过程中,为了给用户提供反馈,我们可以添加加载动画。例如,一个旋转的加载指示器。
<script>
function spin(node, params) {
const start = window.performance.now();
const end = start + (params.duration || Infinity);
let rafId;
function tick() {
const elapsed = window.performance.now() - start;
const rotation = (elapsed / 1000) * 360;
node.style.transform = `rotate(${rotation}deg)`;
rafId = requestAnimationFrame(tick);
}
rafId = requestAnimationFrame(tick);
return {
duration: Infinity,
destroy: () => cancelAnimationFrame(rafId)
};
}
let isLoading = true;
setTimeout(() => {
isLoading = false;
}, 3000);
</script>
{#if isLoading}
<div transition:spin="{{ duration: Infinity }}">
<div class="loader"></div>
</div>
{/if}
<style>
.loader {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 30px;
height: 30px;
border - radius: 50%;
border - top - color: #07c;
animation: spin 1s ease - in - out infinite;
-webkit - animation: spin 1s ease - in - out infinite;
transform: translateZ(0);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
在上述代码中,spin
动画函数使加载指示器持续旋转。通过 requestAnimationFrame
来控制旋转的帧率,确保动画的流畅性。同时,利用 CSS 的 animation
属性也定义了一个备用的旋转动画,以确保在不支持 Svelte 动画的情况下也能有基本的加载效果。当数据加载完成(这里通过 setTimeout
模拟),加载动画停止显示。