본문 바로가기
Web과 프로그래밍 언어/JavaScript

[SVELTE] Transitions - The transition directive, Adding parameters, In and out, Custom CSS transitions, Custom JS transitions, Transition events, Local transitions, Deferred transitions, Key blocks

by cosmicgy 2023. 3. 11.

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}

 

- 트랜지션의 종류

  1. fade 트랜지션 : fade 트랜지션은 요소의 opacity를 조절하는 트랜지션/ 불투명에서 투명으로 투명에서 불투명으로 변경된다
  • 파라미터 : dealy, duration, easing

 

  1. blur 트랜지션 : 요소를 흐릿하게 보였다가 뚜렷하게 보이도록 변경하거나 점차 요소를 흐릿하게 하여 요소를 제거한다.
  • 파라미터 : delay, duration, easing, opacity, amount

 

  1. fly 트랜지션 : 요소가 날아오면서 요소를 추가, 제거하는 트랜지션
  • 파라미터 : delay, duraiton, easing, x, y, opacity

 

  1. slide 트랜지션 : 위에서 아래로 슬라이드로 나타내거나 제거되는 트랜지션
  • 파라미터 : delay, duration, easing

 

  1. scale 트랜지션 : opacity와 scale을 사용하는 트랜지션
  • 파라미터 : delay, duration, easing, start, opacity

 

  1. draw 트랜지션 : SVG요소의 선을 그리듯이 화면에 나타내는 트랜지션
  • 파라미터 : delay, speed, duration, easing

 

  1. 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에 입력되고 삭제되는 것처럼 보여지지 않도록 해준다.