MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

掌握 Svelte 自定义动画:从基础到高级的实现技巧

2021-03-143.6k 阅读

一、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 提供了一些内置的动画函数,这些函数可以帮助我们快速实现常见的动画效果。

  1. 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}
  1. slide:滑动动画。通过改变元素的 transform 属性(通常是 translateYtranslateX)来实现元素的滑动效果。
<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 指令来创建自定义动画。首先,定义一个函数,该函数接受目标元素以及一些配置参数,并返回一个包含 durationtick 函数的对象。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 中移除)。

  1. 进入动画:当元素首次渲染到 DOM 中时,transition 指令中的动画函数会被调用,进入动画开始执行。例如,我们上面定义的 myCustomTransition 函数在元素首次显示时会使元素从初始位置开始水平移动。
  2. 更新动画:当元素的某些属性发生变化时,可以触发更新动画。要实现更新动画,我们需要在 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 中的 xy 属性进行动画操作。通过链式调用 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 动画性能优化

  1. 硬件加速:利用 CSS 的 will-change 属性来提示浏览器提前准备动画所需的资源,从而提高动画性能。例如,在自定义动画函数中,可以在动画开始前设置 node.style.willChange = 'transform';,这样浏览器会提前为元素的 transform 动画做优化准备。
  2. 减少重排和重绘:尽量避免在动画过程中频繁改变元素的布局属性(如 widthheight 等)。如果必须改变这些属性,可以考虑使用 transform 来模拟类似的效果。例如,要实现元素的放大效果,使用 transform: scale(2); 比直接改变 widthheight 属性更高效,因为 transform 动画不会触发重排,只会触发重绘。
  3. 使用 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 的 perspectivebackface - 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 模拟),加载动画停止显示。