Svelte 官方入门教程(9) - 动画

svelte cover

本讲是动画,包含了运动(motion)、过渡(transition)和动画(animations),动画可以改变页面的的呆板,给人以生动活泼的感觉,适当使用动画能给我们的系统增色不少,svelte在动画方面的api非常的出色,使用起来也很简单。

一、运动

1.补间动画(tweened)

设置值并自动观察 DOM 更新很酷。 知道什么更酷吗? 补间这些值。 Svelte 包含的工具可帮助您构建使用动画来传达更改的流畅用户界面。
看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
import { writable } from 'svelte/store';

const progress = writable(0);
</script>

<progress value={$progress}></progress>

<button on:click="{() => progress.set(0)}">
0%
</button>

<button on:click="{() => progress.set(0.25)}">
25%
</button>

<button on:click="{() => progress.set(0.5)}">
50%
</button>

<button on:click="{() => progress.set(0.75)}">
75%
</button>

<button on:click="{() => progress.set(1)}">
100%
</button>

<style>
progress {
display: block;
width: 100%;
}
</style>

上面的例子我们点击下面的按钮就可以看到对于的效果,但是这些效果没有动画效果。

让我们首先将 progress store 更改为补间值:

1
2
3
4
5
<script>
import { tweened } from 'svelte/motion';

const progress = tweened(0);
</script>

单击按钮可使进度条动画化为其新值。但这有点机械化,令人不满意。我们需要添加一个缓解功能:

1
2
3
4
5
6
7
8
9
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';

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

看上述的代码我们发现修改了duration的值,还有缓动(easing)

svelte/easing 模块包含了 Penner easing 缓动方程,你也可以使用自己的 p => t 函数,其中 p 和 t 都是 0 ~ 1 之间的值。

tweened 效果可用的全套选项如下:

  • delay — 补间延迟多少毫秒之后开始
  • duration — 补间的持续时间(以毫秒为单位)或 (from, to) => milliseconds,允许您(例如)指定更长的补间以实现更大的值变化
  • easing — 一个p => t 函数
  • interpolate — 自定义 (from, to) => t => value,用于在任意值之间进行插值。 默认情况下,Svelte 将在数字、日期和形状相同的数组和对象之间进行插值(只要它们只包含数字和日期或其他有效的数组和对象)。 如果您想插值(例如)颜色字符串或转换矩阵,请提供自定义插值器

你还可以将这些选项作为第二个参数传递给 progress.setprogress.update,在这种情况下,它们将覆盖默认值。setupdate 方法都返回一个Promise,当补间效果完成时,Promise 将被 resolve

REPL

2.弹簧动画(Spring)

spring 函数是 tweened 的替代方法,它通常更适用于经常变化的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script>
import { writable } from 'svelte/store';

let coords = writable({ x: 50, y: 50 });
let size = writable(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>

在上面的例子中,我们有两个 stores——一个代表圆的坐标,一个代表它的大小。 让我们将它们转换为弹簧动画:

1
2
3
4
5
6
<script>
import { spring } from 'svelte/motion';

let coords = spring({ x: 50, y: 50 });
let size = spring(10);
</script>

两个弹簧都有默认的 stiffness(强度)damping values(阻尼值),它们控制弹簧的,嗯……弹性。 我们可以指定自己的初始值:

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

左右摆动鼠标,并尝试拖动滑块以了解它们如何影响弹簧的行为。 请注意,您可以在弹簧仍在运动时调整这些值。

有关更多信息,请参阅 API 参考。

REPL

二、过渡

1. transition 指令

我们可以通过优雅地将元素移入和移出 DOM 来制作更吸引人的用户界面。 Svelte 使用过渡指令使这变得非常容易。

看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
let visible = true;
</script>

<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>

{#if visible}
<p>
Fades in and out
</p>
{/if}

上述例子就是点击checkbox进行切换下面的p标签的显示和隐藏。现在我们使用transition来改造这个例子。

首先,从 svelte/transition 导入淡入淡出功能…

1
2
3
4
<script>
import { fade } from 'svelte/transition';
let visible = true;
</script>

然后将其添加到 <p> 元素:

1
<p transition:fade>Fades in and out</p>

这个时候点切换,p标签的显示隐藏就有淡入淡出的效果了。

2. 附加参数

还是使用上面的例子,转换函数可以接受参数。 用 fly 替换淡入淡出过渡…

1
2
3
4
<script>
import { fly } from 'svelte/transition';
let visible = true;
</script>

并将fly与一些选项一起应用于 <p>

1
2
3
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>

请注意,转换是可逆的——如果在转换进行时切换复选框,它会从当前点转换,而不是开始或结束。

3. in 和 out

除了用transition指令来过渡外,元素也可以使用一个in或一个out指令来过渡,或者两者齐用。

fly旁边再导入fade

1
import { fade, fly } from 'svelte/transition';

然后用单独的 inout 指令替换 transition 指令:

1
2
3
<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>
Flies in, fades out
</p>

在这种情况下,过渡效果则是 不可逆 的了。

REPL

4. 自定义 CSS 过渡

svelte/transition 模块含有一些内置的过渡效果,但是创建自己的过渡效果也是非常容易,举例来说,这是 fade过渡的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
function fade(node, {
delay = 0,
duration = 400
}) {
const o = +getComputedStyle(node).opacity;

return {
delay,
duration,
css: t => `opacity: ${t * o}`
};
}

该函数接收两个参数(过渡应用到节点以及传入的任何参数)并返回一个过渡对象,该对象可以具有以下属性:

  • delay : 过渡开始(毫秒)。
  • duration: 过渡时长(毫秒)。
  • easing :p => t easing 函数
  • css :(t, u) => css函数, where u === 1 - t。
  • tick — a (t, u) => {…} 对节点有

t 值为 0时表示开始,值为1 表示结束,根据情况含义可以截然相反。

大多数情况下,您应该返回该 css 而不是tick 属性,因为 CSS animations 会运行在主线程中,以避免出现混淆。.Svelte ‘模拟(simulates)’ 过渡效果并创建CSS animation,然后使其运行。

例如,fade 过渡会生成 CSS animation ,如下所示:

1
2
3
4
5
0% { opacity: 0 }
10% { opacity: 0.1 }
20% { opacity: 0.2 }
/* ... */
100% { opacity: 1 }

不过我们可以发挥更大的创新,做出真正定制化的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
import { fade } from 'svelte/transition';
import { elasticOut } from 'svelte/easing';

let visible = true;

function spin(node, { duration }) {
return {
duration,
css: t => {
const eased = elasticOut(t);

return `
transform: scale(${eased}) rotate(${eased * 1080}deg);
color: hsl(
${~~(t * 360)},
${Math.min(100, 1000 - 1000 * t)}%,
${Math.min(50, 500 - 500 * t)}%
);`
}
};
}
</script>

记住:能力越大责任越大。

REPL

5. 自定义js动画

虽然通常应该尽可能多地使用CSS进行过渡,但是如果不借助JavaScript,有些效果是无法实现的,例如“逐字打印”效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
let visible = false;

function typewriter(node, { speed = 1 }) {
const valid = (
node.childNodes.length === 1 &&
node.childNodes[0].nodeType === Node.TEXT_NODE
);

if (!valid) {
throw new Error(`This transition only works on elements with a single text node child`);
}

const text = node.textContent;
const duration = text.length / (speed * 0.01);

return {
duration,
tick: t => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
</script>

<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>

{#if visible}
<p transition:typewriter>
The quick brown fox jumps over the lazy dog
</p>
{/if}

6. 过渡事件

了解过渡的开始和结束可能会很有用。Svelte调度监听事件,像监听其他任何DOM事件一样:

1
2
3
4
5
6
7
8
9
<p
transition:fly="{{ y: 200, duration: 2000 }}"
on:introstart="{() => status = 'intro started'}"
on:outrostart="{() => status = 'outro started'}"
on:introend="{() => status = 'intro ended'}"
on:outroend="{() => status = 'outro ended'}"
>
Flies in and out
</p>

事件

  • introstart:进入效果开始
  • introend:进入效果结束
  • outrostart:退出效果开始
  • outroend:退出效果结束

7. 局部过渡(Local transitions)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
import { slide } from 'svelte/transition';

let showItems = true;
let i = 5;
let items = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
</script>

<label>
<input type="checkbox" bind:checked={showItems}>
show list
</label>

<label>
<input type="range" bind:value={i} max=10>

</label>

{#if showItems}
{#each items.slice(0, i) as item}
<div transition:slide>
{item}
</div>
{/each}
{/if}

<style>
div {
padding: 0.5em 0;
border-top: 1px solid #eee;
}
</style>

无论是添加或销毁任何标签容器块,过渡都会在标签上播放,示例中,单个列表项的过渡效果影响到切换整个列表,以致切换可见性时也有过渡效果。

如果我们仅仅是想在列表项的新增和删除时出现过渡效果,也就是说,当用户拖动滑块时)播放过渡,那该如何做呢?

我们现在就可以通过 局部(local)过渡来实现该功能,

1
2
3
<div transition:slide|local>
{item}
</div>

看看下面例子来加深理解:

REPL

8. 延迟过渡(Deferred transitions)

Svelte 过渡引擎其中一项特别强大的功能就是可以设置延时(defer) 过渡,以便多个效果之间协调。

下面我们通过一个穿梭框的例子来加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<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>
<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">
<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>

以这todo lists为例, 在其中更换todo将其发送到相对的列表中。在真实世界中,物体的移动不是这般生硬,它们是有其运动轨迹,而不像这般突然出现。给程序添加运动效果能更好的帮助用户了解程序界面变化。

我们可以使用crossfade 函数来实现此效果,该函数创建一对称名为 sendreceive. 当一个标签被 'sent'时, 它会寻找一个被'received'的标签,并赋予一个过渡效果,反之同理。如果没有对应的接收方,过渡效果将会设置为fallback

在65行找到 <label>标签, 给它添加sendreceive 过渡:

1
2
3
4
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>

对下一个 <label> 标签执行相同的操作:

1
2
3
4
5
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>

现在,当您切换列表项时,它们会平滑移动到新位置,没有添加过渡效果的标签仍然笨拙地跳来跳去,我们将下一章中解决它。

这个例子复杂一些可以参考下面的REPL加深理解

三、动画效果

动画指令

在前一章中,我们使用延迟转换来创建元素从一个待办事项列表移动到另一个待办事项列表时的运动错觉。

为了完成幻觉,我们还需要对没有过渡的元素应用运动。 为此,我们使用 animate 指令。

首先,从 svelte/animate 中导入flip——翻转代表“First, Last, Invert, Play”

1
import { flip } from 'svelte/animate';

然后将其添加到 <label> 元素中:

1
2
3
4
5
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>

在这种情况下移动有点慢,所以我们可以添加一个 duration 参数:

1
2
3
4
5
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip="{{duration: 200}}"
>

duration 也可以是 d => milliseconds 函数,其中 d 是元素必须经过的像素数

请注意,所有的过渡和动画都是通过 CSS 而不是 JavaScript 应用的,这意味着它们不会阻塞(或被阻塞)主线程。

对比在上面例子中添加了动画指令之后的效果,来加深印象。

总结

  • Svelte 支持两种运动效果,一种是补间动画效果 tweened,另一种是弹簧效果 spring。这两种效果在页面中都十分常见,因此 Svelte 内置支持它们。
  • svelte/transition (过渡) 模块具有六个函数: fade, fly, slide, scale, draw 和 crossfade。 它与 svelte 一起使用。
  • transition 还是可以animations一起使用。

Svelte 官方入门教程(9) - 动画
http://yoursite.com/2022/07/15/svelte-tutorial-9/
作者
昂藏君子
发布于
2022年7月15日
许可协议