[Vue.js] Vue 애니메이션(Animate) #3 - GSAP 3

하수도키

·

2021. 5. 1. 23:17

728x90
반응형
SMALL

[Vue.js] Vue 애니메이션(Animate) #3 - GSAP 3

  • 이번 포스팅은 Vue 애니메이션에 대해 알아보자.
  • 애니메이션을 이용해 사용자 경험을 향상할 수 있다.
    • 버튼 같은 곳에 마우스 호버 상태 일 때 클릭률을 높이거나
    • 강조해야 되는 문구, 영역 등에 애니메이션을 적용해 집중력을 높일 수 있다.

목차

  1. Transition
    1. Page Transition
    2. Transition Group
  2. Javascript Hooks + Velocity
  3. GSAP(이번 포스팅)

개요

이전 포스팅에서는 Velocity.js 라이브러리를 사용했다.

이번 포스팅에선 GSAP(Green Sock Animation Platform) 강력하고 인기 있는 애니메이션 라이브러리를 Vue 에 적용하고 사용하는 법을 알아보자.

GSAP이란?(What's GSAP?)

GSAP은 높은 성능과 굳건한 라이브러리이다. 커뮤니티에서 애니메이션 관련 검색하면 쉽게 찾을 수 있다.

이 전 라이브러리는 4개의 섹션(TweenLite, TweenMax, TimelineLite, TimelienMax) 쪼개졌으나 GSAP3이 나오면서 1개의 섹션으로 구성되어 있다.

작동방식은 애니메이션이 될 타겟을 지정하고 애니메이션 옵션도 같이 객체로 넘기면 된다.

말로는 그만 설명하고 코드를 보면서 살펴보자.

첫 GSAP 애니메이션(Our First GSAP Animation)

GSAP 를 하기 전에 우선 기본 예제 코드를 살펴보자.

<template>
  <transition appear @before-enter="beforeEnter" @enter="enter" :css="false">
    <div class="card"></div>
  </transition>
</template>

<script>
  export default {
    methods: {
      beforeEnter(el) {
        // starting style
      },
      enter(el, done) {
        // style to transition to once entered
      },
    },
  };
</script>

<style scoped>
  .card {
    display: block;
    margin: 0 auto 0 auto;
    height: 6.5em;
    width: 6.5em;
    border-radius: 1%;
    background-color: #16c0b0;
    box-shadow: 0.08em 0.03em 0.4em #ababab;
  }
</style>
  • transition 컴포넌트를 사용하고 card div를 감싼다.
  • appear 속성을 사용해 첫 로드시에도 애니메이션을 작동한다.
  • css:falseadding/removing 클래스 속성이 추가 안되게 한다.
  • Javascript Hooks beforeEnter, enter 를 사용
  • 자세한 내용은 이전 포스팅을 참고하자.
  • 예제 코드 : https://codesandbox.io/s/dreamy-ramanujan-7kyfo?file=/src/components/Stagger.vue

이제 GSAP을 적용해보자.

  • GSAPimport
  • beforeEnter 에 보이기 전 스타일 정의
  • entergsap 을 이용해 보일 때 애니메이션 정의
<script>
  import gsap from 'gsap'
  export default {
      ...,
  methods: {
    beforeEnter(el) {
      el.style.opacity = 0
      el.style.transform = 'scale(0,0)'
    },
      enter(el, done) {
        gsap.to(el, {
          duration: 1,
          opacity: 1,
          scale: 1,
          onComplete: done
        })
      }
  }
</script>

gsap 을 사용한 코드를 보면 to 메서드를 사용했다.

첫 번째 인자는 애니메이션이 될 타겟 element 를 넣었고, 두 번째 인자에는 타겟이 종료될 때 스타일과 애니메이션 관련 속성들, 그리고 애니메이션이 완료될 때 실행할 메서드를 전달할 수 있는 속성(onComplete)도 있습니다. 이를 통해 transition 컴포넌트가 이번 Lifecycle 이 완료되었음을 알고 다음 hook 으로 이동할 수 있다.

지금도 애니메이션 잘 작동하지만 ease 속성을 이용해 카드가 튀어 오르는 효과를 넣어보자.

enter(el, done) { 
  gsap.to(el, { 
    duration: 1, 
    opacity: 1, s
    cale: 1, 
    ease: 'bounce.out', onComplete: done 
  }) 
}

easing 옵션은 여기 링크를 참조하자.

https://greensock.com/docs/v2/Easing

Stagger Elements

Stagger 효과를 내는 법을 알아보자. 모르겠는데 휘청휘청한 효과라고 한다.

라이브러리를 사용해 애니메이션을 구축할 땐, transition/transition-group 컴포넌트를 굳이 사용하지 않아도 된다.

이번 예제는 mounted 에서 처리해보자.

<template>
  <div id="container">
    <div v-for="card in cards" :key="card.id" class="card"></div>
  </div>
</template>

<script>
  import gsap from "gsap";

  export default {
    data() {
      return {
        cards: [
          { id: 1298 },
          { id: 8748 },
          { id: 4919 },
          { id: 5527 },
          { id: 9428 },
          { id: 7103 },
        ],
      };
    },
    mounted() {
      // stagger cards into position
      gsap.from(".card", {
        duration: 0.5,
        opacity: 0,
        scale: 0,
        y: 200,
        ease: "power1",
        stagger: 0.1,
      });
    },
  };
</script>

<style scoped>
  #container {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-evenly;
  }

  .card {
    height: 6.5em;
    width: 6.5em;
    border-radius: 1%;
    background-color: #16c0b0;
    box-shadow: 0.08em 0.03em 0.4em #ababab;
    padding-top: 1em;
    margin-top: 0.5em;
    margin-right: 0.5em;
  }
</style>

gsap.from 을 사용한 이유는 보이기 전 시작할 때 정보를 제공한다.

첫 화면에 보일 때 스타일 지정하고 마지막은 기본 스타일로 보인다.

gsap 에 애니메이트 될 타깃하고 애니메이션 속성들을 전달해주면 된다.

stagger 속성은 다음 카드가 애니메이션 될 때 지연되는 시간을 입력하면 된다.

여기까지는 평범한 stagger 좀 더 다양한 효과를 추가해보자.

예제 코드 : https://codesandbox.io/s/dreamy-ramanujan-7kyfo?file=/src/components/Stagger.vue

mounted() { gsap.from('.card', { duration: 0.5, opacity: 0, scale: 0, y: 200,
ease: 'power1', stagger: { from: 'edges', each: 0.1 } }) }

위와 같이 stagger 속성에 객체로 더 추가하면 다양한 효과를 낼 수 있다.

아래 링크를 통해 다양한 속성을 알 수 있다.

https://codepen.io/GreenSock/full/vYBRPbO

상태 변경에 따른 GSAP 활용하기(State with GSAP)

실시간 점수 업데이트 또는 회사 주가 업데이트가 될 경우 데이터 상태에 따라 멋지게 보이는 방법을 살펴보자. GSAP 내부적으로 tweens 를 사용한다.

(트위닝이란 애니메이션의 시작과 끝 장면의 그림이 있으면 그 중간 장면들은 자동으로 생성하여 연결되는 방식입니다.)

예를 들면 1에서 10까지 값이 있으면, 2,3,4,5,6,7,8,9 값들이 중간에 존재한다. GSAP을 이용해 이 값 사이를 트위닝 할 수 있다.

예제 코드를 보면서 살펴보자.

<template>
  <div>
    <div :style="{ width: number + 'px' }" class="bar">
      <span>{{ number }}</span>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        number: 0,
      };
    },
    methods: {
      randomNumber() {
        this.number = Math.floor(Math.random() * (800 - 0));
      },
    },
    created() {
      setInterval(this.randomNumber, 1500);
    },
  };
</script>

<style scoped>
  .bar {
    padding: 5px;
    background-color: #2c3e50;
    border: 1px #16c0b0 solid;
    min-width: 20px;
  }
  .bar span {
    color: white;
  }
</style>

created 라이프 사이클에 setInterval 설정을 해서 1.5초마다 number 를 갱신한다.

numberdiv style 에 바인딩한다.

number 가 변경될 때마다 div width 값이 변경된다.

지금은 애니메이션 적용이 되지 않은 상태이다. 이제 GSAP 를 적용해 부드러운 애니메이션 효과를 넣어보자.

완성된 예제 코드를 먼저 살펴보고 차례차례 알아보자

<template>
  <div>
    <div :style="{ width: tweenedNumber.toFixed(0) + 'px' }" class="bar">
      <span>{{ tweenedNumber.toFixed(0) }}</span>
    </div>
  </div>
</template>

<script>
  import { gsap } from "gsap";

  export default {
    data() {
      return {
        number: 0,
        tweenedNumber: 0,
      };
    },
    watch: {
      number(newValue) {
        gsap.to(this.$data, {
          duration: 1,
          ease: "circ.out",
          tweenedNumber: newValue,
        });
      },
    },
    methods: {
      randomNumber() {
        this.number = Math.floor(Math.random() * (800 - 0));
      },
    },
    created() {
      setInterval(this.randomNumber, 1500);
    },
  };
</script>

<style scoped>
  .bar {
    padding: 5px;
    background-color: #2c3e50;
    border: 1px #16c0b0 solid;
    min-width: 20px;
  }
  .bar span {
    color: white;
  }
</style>

우선 gsapimport 한다.

그 다음, watch 를 사용해 number 가 업데이트 될때마다 필요한 로직을 작성하자.

[gsap.to](http://gsap.to) 를 사용해 data 에 정의된 tweenedNumber 을 업데이트 하면 tweenedNumberbar width 가 변경되어 애니메이션이 된다.

watch: { number(newValue) { gsap.to(this.$data, {}) } },

기존에는 첫번째 전달 인자에 애니메이션이 될 element 를 넣었는데 여기서는 this.$data 를 넣었다. 이제 data 에 접근도 가능하고 업데이트도 가능하다.(여기서는 tweendNumber 를 업데이트한다.) 그리고 두 번째 전달 인자에는 기존과 같이 애니메이션 정보를 객체 형식으로 넣어주면 된다.

예제 코드 : https://codesandbox.io/s/crazy-sara-q0b8g?file=/src/components/HelloWorld.vue:0-856

GSAP을 이용해 Timelines 구현하기(Timelines with GSAP)

GSAP을 사용하면서 점점 애니메이션 복잡성이 올라갈 때가 있다.(다수의 엘리먼트들이 순서대로 애니메이션 효과를 보여줘야 할 때)

이때, GSAP에 timeline 기능을 사용하면 구현이 가능하다.

완성된 예제 코드를 우선 보고 하나하나씩 살펴보자.

<template>
  <div>
    <img
      class="runner first"
      width="300px"
      src="https://image.freepik.com/free-icon/running-man_318-1564.jpg"
      alt="runner"
    />
    <img
      class="runner second"
      width="300px"
      src="https://image.freepik.com/free-icon/running-man_318-1564.jpg"
      alt="runner"
    />
    <img
      class="runner third"
      width="300px"
      src="https://image.freepik.com/free-icon/running-man_318-1564.jpg"
      alt="runner"
    />
  </div>
</template>

<script>
  import gsap from "gsap";

  export default {
    mounted() {
      // timeline will go here
      let tl = gsap.timeline();
      tl.to(".first", { x: 700, duration: 2, ease: "expo.out" });
      tl.to(".second", { x: 700, duration: 2, ease: "expo.out" });
      tl.to(".third", { x: 700, duration: 2, ease: "expo.out" });
    },
  };
</script>

<style scoped>
  .runner {
    display: block;
    height: 5em;
    width: 5em;
    margin-top: 1.5em;
  }
</style>

우선 mounted 에서 타임라인 로직을 넣어 dom 이 모두 렌더링이 되었을때 실행되게 하자.

mounted() { 
  // timeline will go here 
  let tl = gsap.timeline(); // gsap timeline을 호출한다. 
  tl.to(".first", { x: 700, duration: 2, ease: "expo.out" });
  tl.to(".second", { x: 700, duration: 2, ease: "expo.out" }); 
  tl.to(".third", { x: 700, duration: 2, ease: "expo.out" }); },

to 메소드 첫 번째 인자의 해당 element 를 넣고 두번째 인자의 애니메이션 옵션을 넣는다.

x 는 시작 위치에서 x축으로 700px 이동하라는 속성이고, duration 지속시간이다.

위 예제를 실행시키면 순서대로 차례차례 애니메이션 이동된다.

.first 요소가 움직이고 그 다음 .second 요소가 움직이고 마지막으로 .third 요소가 움직인다.

총 6초가 소요된다.

이제 순서대로 말고, 애니메이션 시작하는 시간을 변경하는 방법을 알아보자.

2가지 방법이 있다. Absolute PositionRelative Position 이 있다.

(CSS에서 사용하는 용어랑 같아서 혼란이 있을 수 있다. 전혀 다른 거니깐 연관 짓지 말자.)

mounted() { 
  ... 
  tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' })
  tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, 0.5) // now with an absolute position 
  tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }) }

우선 Absolute Position 예제를 보면 0.5 즉, 0.5초 뒤에 실행한다는 뜻이다.

어떤 조건이든 상관없이 mounted 되자마자 0.5초뒤에 .second 애니메이션 시작이다.

mounted() { 
  ... 
  tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' })
  tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '-=.75')
  tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }) }

이번에는 Relative Position 이다. 첫번째 .first 요소가 애니메이션 끝나기전 0.75 초전에 second 요소 애니메이션이 실행된다.

이번에는 .first 요소가 끝나거나 시작할때 .second 요소도 같이 애니메이션을 시작하고 싶다.

여태까지 살펴본 내용으로 .first.second 동시에 시작하기 위한 코드는 아래와 같이 작성한다.

mounted() { 
  ... tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' })
  tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '-=2')
  tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }) }

작동은 잘된다. 하지만 .firstduration 값이 변경될 경우 그에 맞게 다시 -=2 이쪽 부분을 수정해야 한다. 너무너무 번거롭다.

이때 < 이걸 사용하면 손쉽게 해결이 된다.

mounted() { 
  ... 
  tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' })
  tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '<') // starts when first tween starts 
  tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }, '<') // starts when second tween starts, which is when first tween starts }

<0.5 이렇게 사용하면 0.5 초 뒤에 실행한다.

mounted() { 
  ... 
  tl.to('.first', { x: 200, duration: 2, ease: 'expo.out' })
  tl.to('.second', { x: 400, duration: 2, ease: 'expo.out' }, '<0.5')  // What will this do? 
  tl.to('.third', { x: 600, duration: 2, ease: 'expo.out' }) }

.first 가 실행되고 0.5초뒤에 .second 가 실행된다.

타임라인에 대한 다양한 position 옵션을 살펴보려면 이 링크를 참조하자.https://greensock.com/docs/v3/GSAP/Timeline

타임라인 반복 기능은 repeat 속성을 사용해 정의한다.

gsap.timeline({ repeat: 2 }) // 2번 반복 
gsap.timeline({ repeat: -1 }) // 무한 반복 
gsap.timeline({ repeat: -1, repeatDelay: 1 }) // 반복시 딜레이 1초

마지막으로, 여러 개의 타임라인 애니메이션을 사용하는 방법을 살펴보자.

완성된 예제 코드부터 우선 살펴보자.

<template>
  <div>
    <div id="container">
      <img class="paws" id="paw1" src="../assets/paws.png" alt="fox-paws" />
      <img class="paws" id="paw2" src="../assets/paws.png" alt="fox-paws" />
      <img class="paws" id="paw3" src="../assets/paws.png" alt="fox-paws" />
      <img class="paws" id="paw4" src="../assets/paws.png" alt="fox-paws" />

      <img id="fox" src="../assets/fox.png" alt="fox-logo" />
    </div>

    <button @click="play">Play</button>
  </div>
</template>

<script>
  import gsap from "gsap";

  let masterTL = gsap.timeline();

  export default {
    methods: {
      play() {
        masterTL.add(this.pawsTL());
        masterTL.add(this.foxTL());
        masterTL.play();
      },
      pawsTL() {
        let tl = gsap.timeline();
        tl.to("#paw1", {
          opacity: 4,
          scale: 1,
          duration: 0.5,
          ease: "bounce.out",
        });
        tl.to(
          "#paw2",
          { opacity: 1, scale: 1, duration: 0.5, ease: "bounce.out" },
          "<.3"
        );
        tl.to(
          "#paw3",
          { opacity: 1, scale: 1, duration: 0.5, ease: "bounce.out" },
          "<.3"
        );
        tl.to(
          "#paw4",
          { opacity: 1, scale: 1, duration: 0.5, ease: "bounce.out" },
          "<.3"
        );
        return tl;
      },
      foxTL() {
        let tl = gsap.timeline();
        tl.to(
          "#fox",
          {
            opacity: 1,
            filter: "blur(0)",
            scale: 1,
            duration: 0.4,
            ease: "slow",
          },
          "<.2"
        );
        return tl;
      },
    },
  };
</script>

<style scoped>
  #container {
    display: flex;
    flex-direction: row;
    justify-content: center;
    margin-top: 5em;
  }

  #fox {
    height: 8em;
    width: 8em;
    opacity: 0;
    filter: blur(2px);
  }

  .paws {
    transform: scale(0);
    width: 2.5em;
    height: 2.5em;
    margin-top: 50px;
    margin-right: 0.8em;
    opacity: 0;
  }

  button {
    margin-top: 5em;
  }
</style>

.paws 타임라인이랑 #fox 타임라인 2개를 사용할때 코드이다.

masterTL 이라는 타임라인을 생성하고 거기에 add 메서드를 이용해 타임라인을 추가하면 된다.

play() { 
  masterTL.add(this.pawsTL()) 
  masterTL.add(this.foxTL()) 
  masterTL.play()
},

이 부분이 핵심이다.

각 타임라인을 추가한 다음 play() 메서드를 사용해 실행한다.

이렇게 하면 모듈화가 가능하고 재사용성도 좋고 확장성도 좋아진다.

결론

이 포스팅을 마지막으로 Vue 애니메이션 살펴보기를 마친다.

애니메이션 관련된 기초적인 부분들로만 다뤘고 좀 더 깊은 내용을 살펴보려면 해당 공식 사이트를 참고하여 살펴보는 것을 추천합니다.

이 포스팅을 바탕으로 좋은 애니메이션을 Vue에서 잘 구현하길 바랍니다.

728x90
반응형
LIST