Svelte中的动画

作者:renzp94
时间:2021-03-26 02:52:07

motion

Svelte提供一个svelte/motion模块,用于支持动画,此模块有两个函数:tweenedspring,通过这两个函数可以创建一个store,然后通过set设置和update更新其值。

tweened

通过tweened创建的值当发生改变时,不会立即更新而是随着时间动态的更新。tweened主要接受两个参数:

  • value:初始值
  • option
    • deplay:延时执行的时间(毫秒)
    • duration:持续时间(毫秒)
    • easing:动画函数。可以使用svelte/easing提供的函数
    • interpolate:自定义动画区间的值。必须是(a,b) => t => value格式的函数,其中a为起始值,b为目标值,t是 0~1 之前的数字,value是结果。如:两个颜色间平滑过渡可以用d3-interpolate

在线 REPL

<script>
    import { tweened } from 'svelte/motion';
    import { cubicOut } from 'svelte/easing';

    const progressList = [0, 25, 50, 75, 100];

    let progress = tweened(0, {
        duration: 300,
        easing: cubicOut
    });
</script>

<input type="range" bind:value={$progress} />
<div>
    {#each progressList as item (item)}
        <button on:click={() => progress.set(item)}>{item}%</button>
    {/each}
</div>

spring

spring是对tweened的一个补充用于弹性设置,适用于多个值间的变化。主要接受两个参数:

  • value:初始值
  • option
    • stiffness:0~1,值越高,弹性越大,默认为:0.15
    • damping:0~1,值越高,弹性越小,默认为:0.8
    • precision:精确稳定的值,值越低代表越精确,默认为:0.001

在线 REPL

<script>
    import { spring } from 'svelte/motion';

    let coords = spring(
        { x: 50, y: 50 },
        {
            stiffness: 0.1,
            damping: 0.25
        }
    );

    let size = spring(10);
</script>

<div style="position: absolute; right: 1em;">
    <label>
        <h3>stiffness ({coords.stiffness})</h3>
        <input bind:value={coords.stiffness} type="range" min="0" max="1" step="0.01" />
    </label>

    <label>
        <h3>damping ({coords.damping})</h3>
        <input bind:value={coords.damping} type="range" min="0" max="1" step="0.01" />
    </label>
</div>

<svg
    on:mousemove={(e) => coords.set({ x: e.clientX, y: e.clientY })}
    on:mousedown={() => size.set(30)}
    on:mouseup={() => size.set(10)}
>
    <circle cx={$coords.x} cy={$coords.y} r={$size} />
</svg>

<style>
    svg {
        width: 100%;
        height: 100%;
        margin: -8px;
    }
    circle {
        fill: #ff3e00;
    }
</style>

Transitions

Svelte提供了store/transition模块用于过渡动画,其中有fadeblurflyslidescaledrawcrossfade。可通过transition:xxx来设置inout的动画,也可单独设置动画:in:xxxout:xxx

fade

对元素的不透明度进行动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为linear

在线 REPL

<script>
    import { fade } from 'svelte/transition';
    let status = true;

    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换Hello显示状态 </button>
{#if status}
    <h1 transition:fade>Hello!</h1>
    <h1 out:fade={{ delay: 250, duration: 300 }}>out Hello!</h1>
    <h1 in:fade={{ delay: 250, duration: 300 }}>in Hello!</h1>
    <h1 transition:fade={{ delay: 250, duration: 300 }}>延迟Hello!</h1>
{/if}

blur

对元素的不透明度添加滤镜动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut
  • opacity:不透明度,默认为0
  • amount:模糊大小(单位为像素),默认为5

在线 REPL

<script>
    import { blur } from 'svelte/transition';
    let status = true;

    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换Hello显示状态 </button>
{#if status}
    <h1 transition:blur>Hello!</h1>
{/if}

fly

对元素的xy以及不透明度进行动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut
  • opacity:不透明度,默认为0
  • xx轴偏移量,默认为0
  • yy轴偏移量,默认为0

在线 REPL

<script>
    import { fly } from 'svelte/transition';
    import { quintOut } from 'svelte/easing';

    let status = true;
    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换Hello显示状态 </button>
{#if status}
    <h1
        transition:fly={{ delay: 250, duration: 300, x: 100, y: 100, opacity: 0.5, easing: quintOut }}
    >
        Hello
    </h1>
{/if}

slide

对元素的滑入,滑出进行动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut

在线 REPL

<script>
    import { slide } from 'svelte/transition';
    import { quintOut } from 'svelte/easing';

    let status = true;
    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换Hello显示状态 </button>
{#if status}
    <h1 transition:slide={{ delay: 250, duration: 300, easing: quintOut }}>Hello</h1>
{/if}

scale

对元素的不透明度进行拉远动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut
  • start:拉远比例0~1,默认为0
  • opacity:不透明度,默认为0

在线 REPL

<script>
    import { scale } from 'svelte/transition';
    import { quintOut } from 'svelte/easing';

    let status = true;
    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换Hello显示状态 </button>
{#if status}
    <h1 transition:scale={{ duration: 500, delay: 500, opacity: 0.5, start: 0.3, easing: quintOut }}>
        Hello
    </h1>
{/if}

draw

对 svg 元素的笔触进行动画处理。接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的毫秒数,默认为400
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut
  • speed:转换持续的毫秒数,默认为400

在线 REPL

<script>
    import { draw } from 'svelte/transition';
    import { quintOut } from 'svelte/easing';

    let status = true;
    const onClick = () => (status = !status);
</script>

<button on:click={onClick}> 切换显示状态 </button>
<svg viewBox="0 0 5 5" xmlns="http://www.w3.org/2000/svg">
    {#if status}
        <path
            transition:draw={{ duration: 5000, easing: quintOut }}
            d="M2 1 h1 v1 h1 v1 h-1 v1 h-1 v-1 h-1 v-1 h1 z"
            fill="none"
            stroke="cornflowerblue"
            stroke-width="0.1px"
            stroke-linejoin="round"
        />
    {/if}
</svg>

flip

Svelte还提供了svelte/animate模块,目前只有flipflip计算元素的开始位置和结束位置,并进行动画处理,接受以下参数:

  • delay:延迟的毫秒数,默认为0
  • duration:转换持续的时间(类型:number | function),默认为d => Math.sqrt(d) * 120
    • number:以毫秒为单位。
    • functiondistance: number => duration: number 接受以像素为单位的距离,返回以毫秒为单位的持续时间。
  • easing:过渡动画,可以使用svelte/easing提供的函数,默认为cubicOut

在线 REPL

<script>
    import { quintOut } from 'svelte/easing';
    import { crossfade } from 'svelte/transition';

    const [send, receive] = crossfade({
        duration: (d) => Math.sqrt(d * 200),

        fallback(node, params) {
            const style = getComputedStyle(node);
            const transform = style.transform === 'none' ? '' : style.transform;

            return {
                duration: 600,
                easing: quintOut,
                css: (t) => `
                    transform: ${transform} scale(${t});
                    opacity: ${t}
                `
            };
        }
    });

    let uid = 1;

    let todos = [
        { id: uid++, done: false, description: 'write some docs' },
        { id: uid++, done: false, description: 'start writing blog post' },
        { id: uid++, done: true, description: 'buy some milk' },
        { id: uid++, done: false, description: 'mow the lawn' },
        { id: uid++, done: false, description: 'feed the turtle' },
        { id: uid++, done: false, description: 'fix some bugs' }
    ];

    function add(input) {
        const todo = {
            id: uid++,
            done: false,
            description: input.value
        };

        todos = [todo, ...todos];
        input.value = '';
    }

    function remove(todo) {
        todos = todos.filter((t) => t !== todo);
    }

    function mark(todo, done) {
        todo.done = done;
        remove(todo);
        todos = todos.concat(todo);
    }
</script>

<div class="board">
    <input
        placeholder="what needs to be done?"
        on:keydown={(e) => e.key === 'Enter' && add(e.target)}
    />

    <div class="left">
        <h2>todo</h2>
        {#each todos.filter((t) => !t.done) as todo (todo.id)}
            <label in:receive={{ key: todo.id }} out:send={{ key: todo.id }}>
                <input type="checkbox" on:change={() => mark(todo, true)} />
                {todo.description}
                <button on:click={() => remove(todo)}>remove</button>
            </label>
        {/each}
    </div>

    <div class="right">
        <h2>done</h2>
        {#each todos.filter((t) => t.done) as todo (todo.id)}
            <label class="done" in:receive={{ key: todo.id }} out:send={{ key: todo.id }}>
                <input type="checkbox" checked on:change={() => mark(todo, false)} />
                {todo.description}
                <button on:click={() => remove(todo)}>remove</button>
            </label>
        {/each}
    </div>
</div>

<style>
    .board {
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-gap: 1em;
        max-width: 36em;
        margin: 0 auto;
    }

    .board > input {
        font-size: 1.4em;
        grid-column: 1/3;
    }

    h2 {
        font-size: 2em;
        font-weight: 200;
        user-select: none;
        margin: 0 0 0.5em 0;
    }

    label {
        position: relative;
        line-height: 1.2;
        padding: 0.5em 2.5em 0.5em 2em;
        margin: 0 0 0.5em 0;
        border-radius: 2px;
        user-select: none;
        border: 1px solid hsl(240, 8%, 70%);
        background-color: hsl(240, 8%, 93%);
        color: #333;
    }

    input[type='checkbox'] {
        position: absolute;
        left: 0.5em;
        top: 0.6em;
        margin: 0;
    }

    .done {
        border: 1px solid hsl(240, 8%, 90%);
        background-color: hsl(240, 8%, 98%);
    }

    button {
        position: absolute;
        top: 0;
        right: 0.2em;
        width: 2em;
        height: 100%;
        background: no-repeat 50% 50%
            url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
        background-size: 1.4em 1.4em;
        border: none;
        opacity: 0;
        transition: opacity 0.2s;
        text-indent: -9999px;
        cursor: pointer;
    }

    label:hover button {
        opacity: 1;
    }
</style>

svelte/easing

Svelte提供了以下过渡动画:

ease in out inOut
back backIn backOut backInOut
bounce bounceIn bounceOut bounceInOut
circ circIn circOut circInOut
cubic cubicIn cubicOut cubicInOut
elastic elasticIn elasticOut elasticInOut
expo expoIn expoOut expoInOut
quad quadIn quadOut quadInOut
quart quartIn quartOut quartInOut
quint quintIn quintOut quintInOut
sine sineIn sineOut sineInOut

由此可见,Svelte中内置的动画还是很强大,组合使用可以使用户体验感非常好。