Transitions

The transition directive

Svelte는 transition 선언을 통해 트랜지션을 매우 쉽게 구현할 수 있다.

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

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

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

Adding parameters

트랜지션 함수들은 추가적으로 매개변수를 가질 수도 있다. 이번엔 fade가 아닌 fly의 예를 보자.

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

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

트랜지션이 reversible하다는 점을 눈여겨보자. Svelte에서 제공하는 함수를 통해 구현된 트랜지션은 진행되는 도중에도 다시 되돌아올 수 있다.

In and out

요소가 나타날 때와, 없어질 때 각각의 Transition을 다르게 구현하고자 하는 경우, transition 명령 대신, 요소에 inout 명령을 따로 지정할 수 있다.

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

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

Custom CSS transitions

svelte/transition 모듈에는 자체적으로 유용한 빌트인 트랜지션들이 많이 있으나, 직접 트랜지션을 구성하는 것도 쉽다.

아래는 fade 함수의 소스코드다.

function fade(node, { delay = 0, duration = 400 }) {
  const o = +getComputedStyle(node).opacity;

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

이 함수는 두 개의 매개변수를 받는다. 하나는 트랜지션이 적용될 노트이고, 다른 하나는 아래의 옵션들이다.

  • delay - 트랜지션이 시작되기 전의 딜레이, ms 단위
  • duration - 트랜지션의 전체 길이, ms 단위
  • easing - p => t easing 함수
  • css - (t, u) => css함수, 여기서 u === 1 - t 이다.
  • tick - 노드에 효과를 적용하는 (t, u) => {...} 함수

t는 인트로의 시작 또는 아웃트로의 끝에서 0이고, 인트로의 끝 또는 아웃트로의 시작에서 1이다.

가능하다면 대부분은 tick 프로퍼티가 아닌 css 프로퍼티를 반환해야 하는데, CSS 애니메이션은 가능하다면 브라우저의 버벅거림을 방지하기 위해 메인 스레드에서 실행되지 않기 때문이다. Svelte는 트랜지션을 시뮬레이션하고, CSS 애니메이션을 구성한 뒤 이를 실행한다.

이를테면, fade 트랜지션은 아래와 같은 CSS 애니메이션을 생성한다.

0% {
  opacity: 0;
}
10% {
  opacity: 0.1;
}
20% {
  opacity: 0.2;
}
/* ... */
100% {
  opacity: 1;
}

좀 더 창의적이고 쓸데없는 애니메이션을 하나 만들어보자.

<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(<span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">e</span><span class="mord mathnormal">a</span><span class="mord mathnormal">se</span><span class="mord mathnormal">d</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mord">1080</span></span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mclose">)</span><span class="mpunct">;</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal">co</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">h</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mopen">(</span></span></span></span>{~~(t * 360)},
						<span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10903em;">M</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord">.</span><span class="mord mathnormal">min</span><span class="mopen">(</span><span class="mord">100</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1000</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mord">1000</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mord mathnormal">t</span><span class="mclose">)</span></span></span></span></span>{Math.min(50, 500 - 500 * t)}%
					);`
			}
		};
	}
</script>

Custom JS transitions

일반적으로는 가능하다면 CSS를 이용한 트랜지션을 사용하는 것이 좋지만, 일부 경우에는 JS 없이 구현하기 어려운 효과가 있을 수 있다. 대표적인 것이 타자기 효과다.

<script>
	let visible = false;

	function typewriter(node, { speed = 50 }) {
		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;

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

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

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

Transition events

트랜지션의 시작과 끝이 언제인지를 아는 것이 유용할 때가 있다. Svelte는 다른 DOM 이벤트들과 마찬가지로 해당 시점에 이벤트를 디스패치해준다.

<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>

Local transitions

일반적으로 트랜지션은 컨테이너 블록이 추가되거나 없어지는 모든 경우에 실행된다.

만약, 모든 경우가 아니라, 요소 본인에 대한 직접적인 추가/삭제에 대해서만 트랜지션 효과를 주고자 한다면, local transition을 사용할 수 있다.

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

Deferred transitions

Svelte의 트랜지션 엔진이 갖는 강력한 특징은 트랜지션을 지연시킬 수 있다는 점이다. 따라서, 여러 개의 요소 간에도 이를 조정할 수 있다.

crossfade 함수는 sendreceive라는 두 쌍의 트랜지션을 만들어낸다. 어떤 요소가 send될 때, 해당 요소는 여기에 상응하는 received 요소를 찾고나서, 찾아낸 요소의 위치로 이동하며 트랜지션 효과를 실행한다.

  <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: <span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">an</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mord mathnormal">m</span></span><span class="mord mathnormal">sc</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">e</span><span class="mopen">(</span></span></span></span>{t});
					opacity: {t}
				`
			};
		}
	});

  // ...
  <script>

이후 아래와 주고 받게 될 각각의 요소에서 사용한다.

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

<label
	class="done"
	in:receive="{{key: todo.id}}"
	out:send="{{key: todo.id}}"
>