
本讲是动画,包含了运动
(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.set
和 progress.update
,在这种情况下,它们将覆盖默认值。set
和 update
方法都返回一个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';
|
然后用单独的 in
和 out
指令替换 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
函数来实现此效果,该函数创建一对称名为 send
和receive
. 当一个标签被 'sent'
时, 它会寻找一个被'received'
的标签,并赋予一个过渡效果,反之同理。如果没有对应的接收方,过渡效果将会设置为fallback
。
在65行找到 <label>
标签, 给它添加send
和 receive
过渡:
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一起使用。