transitions
Svelte는 DOM에 요소들이 추가, 제거되었을 때 트랜지션을 효과적으로 지원하는 트랜지션 디렉티브를 제공한다.
1. the transition directive
<script>
import { fade } from 'svelte/transition';
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<p transition:fade>
Fades in and out
</p>
{/if}
2. Adding parameters
트랜지션 함수에 파라미터를 전달하는 것이 가능하다. 데이터를 바인딩 하는 방법과 동일하게 파라미터로 전달하려는 값을 바인딩 하면 된다
<script>
import { fly } from 'svelte/transition';
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>
{/if}
- 트랜지션의 종류
- fade 트랜지션 : fade 트랜지션은 요소의 opacity를 조절하는 트랜지션/ 불투명에서 투명으로 투명에서 불투명으로 변경된다
- 파라미터 : dealy, duration, easing
- blur 트랜지션 : 요소를 흐릿하게 보였다가 뚜렷하게 보이도록 변경하거나 점차 요소를 흐릿하게 하여 요소를 제거한다.
- 파라미터 : delay, duration, easing, opacity, amount
- fly 트랜지션 : 요소가 날아오면서 요소를 추가, 제거하는 트랜지션
- 파라미터 : delay, duraiton, easing, x, y, opacity
- slide 트랜지션 : 위에서 아래로 슬라이드로 나타내거나 제거되는 트랜지션
- 파라미터 : delay, duration, easing
- scale 트랜지션 : opacity와 scale을 사용하는 트랜지션
- 파라미터 : delay, duration, easing, start, opacity
- draw 트랜지션 : SVG요소의 선을 그리듯이 화면에 나타내는 트랜지션
- 파라미터 : delay, speed, duration, easing
- crossfade트랜지션 : 위의 트랜지션과는 조금 차이가 있다.
사용방법
1) import { crossfade } from 'svelte/transition';로 crossfade 함수를 가져온다
2) const [send, receive] = crossfade(params)로 함수를 호출한다.
3) crossfade 함수의 파라미터로 duration, delay, fallback 속성을 포함한 객체를 전달한다.
4) crossfade 함수의 리턴 값은 [send, receive] 형태의 배열이다.
- send: 내보내는 트랜지션, receive 받는 트랜지션/ 두개 모두 유니크한 key 를 가진 객체를 파라미터로 전해야한다
- 받는 대상, 보내는 대상이 없는 경우 fallback에 정의한 트랜지션이 동작하게 된다.
3. In and out
트랜지션 사용시 tranasition 디렉티브 대신에 in, out 디렉티브를 사용할 수 있다. in, out 디렉티브를 사용하면 요소가 추가, 제거될 때마다 각각 다른 트랜지션을 설정할 수 있다.
<script>
import { fly } from 'svelte/transition';
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>
{/if}
요소가 추가될 때는 fly 트랜지션, 제거될 때는 fade 트랜지션이 동작한다.
4. Custom CSS transitions
svelte에서 제공하는 7가지 트랜지션 외의 트랜지션이 필요할 때 원하는 트랜지션을 만들 수 있다.
- 트랜지션 함수의 형태
transition = (node: HTMLElement, params: any) => {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
- 파라미터
- node : 첫번째 파라미터는 트랜지션이 적용되는 HTML요소
- params : transition:fade={params}에 params 로 전달될 값이다. 모든 형태의 값을 전달할 수 있다.
- 리턴값 : 트랜지션 함수는 객체를 리턴해야한다. 리턴하는 객체는 아래의 속성을 갖는다.
- dealy, duration, easing, css, tick
- css와 tick속성을 사용하면 커스텀 한 트랜지션을 만들 수 있다. tick속성은 매 tick마다 호출되는 콜백 함수이기 대문에 tick을 사용해 만들어진 트랜지션은 매끄러운 애니매이션이 동작하지 않을 수 있다. 커스텀 한 트랜지션을 만들 때 css속성을 사용한다.
- css 속성은 (t, u) => css 형태의 함수가 와야 한다. 파라미터인 t(혹은 u)의 변화에 따라 CSS 문자열을 리턴하는 함수를 만들면 된다.
<script>
import { fade } from 'svelte/transition';
import { elasticOut } from 'svelte/easing';
let visible = true;
//css 속성에 t의 변화에 따라 transform과 color의 CSS 문자열을 리턴하는 spin 트랜지션 함수
function spin(node, { duration }) {
return {
duration,
css: t => {
const eased = elasticOut(t);
return `
transform: scale(${eased}) rotate(${eased * 1080}deg);
color: hsl(
${Math.trunc(t * 360)},
${Math.min(100, 1000 - 1000 * t)}%,
${Math.min(50, 500 - 500 * t)}%
);`
}
};
}
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<div class="centered" in:spin="{{duration: 8000}}" out:fade>
<span>transitions!</span>
</div>
{/if}
<style>
.centered {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
span {
position: absolute;
transform: translate(-50%,-50%);
font-size: 4em;
}
</style>
5. Custom JS transitions
트랜지션 함수의 tick 속성을 사용
아래의 코드는 자바스크립트를 사용한 타자 효과를 표현한 트랜지션 예제이다.
<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. Transition events
트랜지션이 언제 끝나고 시작되는지 알려주는 이벤트, 다른 DOM 이벤트와 사용법은 동일하다
<script>
import { fly } from 'svelte/transition';
let visible = true;
let status = 'waiting...';
</script>
<p>status: {status}</p>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<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>
{/if}
7. Local transition
블록 안에서만 트랜지션이 작용하도록 해주는 수식어 local
<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|local>
{item}
</div>
{/each}
{/if}
<style>
div {
padding: 0.5em 0;
border-top: 1px solid #eee;
}
</style>
- slide 가 local 안에서만 적용되며 전체 list를 펼칠 때에는 적용되지 않는다
#each
안에서만 적용, show list에서는 적용되지 않음
8. Deferred transitions
일종의 '지연' 을 일으키는 트랜지션이다. todo list와 같은 것을 만들 때 반대편의 list로 완료한 항목을 보내는 동작을 할 때에 보내는 요소는 바로 등장하지 않고 사라졌다가 다시 나타나는 일종의 지연 현상을 갖는다. 이러한 동작을 send와 receive 함수로 이루어진 crossfade 함수를 호출해 구현시킬 수 있다.
<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>
9. key blocks
key blocks 는 표현식의 값이 변하면 해당 내용을 파괴하고 다시 만드는 기능을 하도록 해준다.
<script>
import { fly } from 'svelte/transition';
let number = 0;
</script>
<div>
The number is:
{#key number}
<span style="display: inline-block" in:fly={{ y: -20 }}>
{number}
</span>
{/key}
</div>
<br />
<button
on:click={() => {
number += 1;
}}>
Increment
</button>
- 여기서 key block 내부의 span 태그 안의 요소가 increment button을 누를 때마다 애니매이션 동작이 이루어지도록 해주는데, 이렇게 요소가 단순히 DOM에 입력되고 삭제되는 것처럼 보여지지 않도록 해준다.