All'alba vincerò

At dawn, I will win!

React

[React] React 애니메이션 (2) - Motion One

나디아 Nadia 2024. 8. 15. 16:22

✅ Motion One

 

⏩ 설치

pnpm add motion

 

 

 

 기능

 

(1) animatie()

: DOM 요소 또는 집합을 쉽게 애니메이션 한다.

import { animate } from 'motion';

animate(target, props, options?)
  • 매개변수
    • 첫 번째 인수: 애니메이션을 적용할 DOM 요소
    • 두 번째 인수: CSS 속성 값(객체 or 배열)
    • 세 번째 인수: 애니메이션 옵션 객체
  • props에 함수를 설정할 수 있다.
animate (
   element, 
   { x: 400, rotate: 360 }, 
   { duration: 1.5, delay: 0.5 }
  );
};


// 함수
animate (
  (progress) => {
    box.textContent = Math.round(progress * 100) + '%';
  }, 
  { duration: 0.5 },
);

 

 

 

☑️ 옵션

duration

: 애니메이션이 완료되는 데 소요되는 시간(초)

  • 기본 값: 0.3
animate(element, { ... }, { duration: 2 });

 

 

delay
: 애니메이션이 시작되기 전, 지연되는 시간(초)

  • 기본 값: 0
animate(element, { ... }, { delay: 0.45 });

 

  • 여러 요소를 애니메이션 할 때, stagger() 함수를 사용하여 각 요소의 지연 시간을 늘릴 수 있다.
import { animate, stagger } from "motion";

animate(
  element,
  { backgroundColor: "red" },
  { delay: stagger(0.1) },
);

 

 

endDelay

: 애니메이션이 종료되기 전, 기다리는 시간(초)

  • 기본 값: 0
animate(element, { ... }, { endDelay: 0.35, repeat: 2 });

 

 

easing
: 전체 애니메이션에 사용되는 이징 리스트

  • 애니메이션 전체에 적용되는 이징 설정
animate(
  "li",
  { transform: "rotate(90deg)" },
  { easing: "steps(2, start)" },
);

 

 

  • 개별 키프레임 간 적용되는 이징 배열 설정
animate(
  element,
  { scale: [0, 1, 2] },
  { easing: ["ease-in", "ease-out"] },
);

 

 

direction

: 애니메이션 재생 방향을 설정

  • normal (기본 값)
  • reverse
  • alternate
  • alternate-reverse
  • 반대 방향에서 애니메이션이 시작 재생되게 하려면 reverse 값 사용
animate(
  element,
  { transform: "rotate(90deg)" },
  { direction: "reverse" },
);

 

 

repeat
: 애니메이션 반복 횟수

  • 무한 반복 설정은 Infinity를 사용
animate(
  element,
  { transform: "rotate(90deg)" },
  { repeat: Infinity },
);
​

 

 

autoPlay

: 애니메이션을 자동 시작할 지 여부를 설정

animate(
  element,
  { transform: "scale(2)" },
  { autoplay: false },
);

 

 

allowWebkitAcceleration

: 애니메이션 성능 향상

  • Webkit의 가속화 애니메이션은 수많은 타이밍 버그가 있기 때문에 Webkit 기반 브라우저는 기본적으로 가속화 기능이 꺼져있음 👉 애니메이션이 무거운 처리로 중단될 경우 이 설정으로 가속 허용 가능⭕
  • 특히 Safari, iOS Chrome에서 애니메이션 테스트를 철처히 하는 것이 좋다.
  • 향후 Webkit 구현이 개선되면 이 옵션의 기본 값은 true로 설정될 것이다.
animate(
  element,
  { transform: "scale(2)" },
  { allowWebkitAcceleration: true },
)

 

 

 

 

☑️ 키프레임

: 지속시간(duration) 전체에 걸쳐 균등하게 간격을 지정하여 애니메이션 설정  

  • 배열을 사용하여 설정
  • 싱글 키프레임과 복합 키프레임 둘 다 사용 가능
// 싱글 키프레임
animate(
 element,
 { 
   opacity: 1, 
   transform: 'rotate(360deg)', 
 },
);

// 복합 키프레임 --------------
animate(
 element,
 { 
   transform: ['rotate(90deg)', 'translateX(100px) rotate(45deg)', 'none'], 
 },
);

 

 

 

☑️ 와일드 카드

: null 값을 정의하여 이미 정의된 값을 대체

  • 이전 키프레임으로 대체할 수 있다.
    ➡ 애니메이션을 조정할 때 한 곳에서만 변경하면 된다. 
element.style.opacity = '0.5';

animate(element, {
  opacity: [null, 0.8, 1]
  // null → 0.5
  // 0.5 → 0.8 → 1
});


// 이전 키프레임으로 대체 ------------
element.style.opacity = '0.5';

animate(element, {
  x: [0, 100, null]
  // null → 100
  // 0 → 100 → 100
});

 

 

 

☑️ 커스텀 키프레임(타이밍)

: 각 키프레임을 애니메이션 전체에 걸쳐 균일한 간격으로 지정

  • 개별 제어가 필요한 경우, 특정 키프레임이 적용되어야 하는 상대 지점을 정의하는 오프셋(offset, 0~1)을 지정할 수 있다.
animate(
  element,
  { color: ['red', 'yellow', 'green', 'blue'], },
  { offset: [0, 0.2] }, // [0, 0.2, 0.6, 1]
);

 

 

 

☑️ 커스텀 키프레임(이징, easing)

: 애니메이션 전체 진행에 사용

  • 이징 옵션을 임의 배열로 정의하면, 특정 키프레임 간 이징 함수를 개별 설정할 수 있다.
animate(
  element,
  { color: ['red', 'yellow', 'green', 'blue'] },
  { easing: ['ease-in', 'linear', 'ease-out'] },
);

 

 


 

(2) timeline()

: 여러 요소에 걸쳐 복잡한 애니메이션 시퀀스(순서, Sequence)를 만들 때 사용

import { timeline } form 'motion';

timeline(sequence, options);
  • 매개변수
    • 첫 번째 인수: 애니메이션을 적용할 DOM 요소 배열
    • 두 번째 인수: 애니메이션 옵션 객체

☑️ 옵션

 

duration

: 타임라인 애니메이션이 완료되는 데 소요되는 시간(초)

  • 기본적으로 재생시간은 시퀀스에 의해 자동 계산된다.
  • 명시적으로 재생시간을 설정한 경우, 전체 애니메이션이 설정 값에 맞게 자동 조정된다.
timeline(sequence, { duration: 4 });



delay

: 타임라인 애니메이션이 시작되기 전, 지연되는 시간(초)

  • 기본 값: 0
timeline(sequence, { delay: 0.5 });

 

 

endDelay

: 타임라인 애니메이션이 종료되기 전, 기다리는 시간(초)

  • 기본 값: 0
timeline(sequence, { endDelay: 0.5 });

 

 

direction

: 타임라인 애니메이션 재생 방향 설정

timeline(sequence, { direction: "alternate", repeat: 2 });

 


repeat
: 타임라인 애니메이션 반복 횟수

  • 기본 값 : 0
  • 무한 반복 설정은 Infinity를 사용
timeline(sequence, { repeat: 2 });

 

 

defaultOptions
: 타임라인 시퀀스의 각 애니메이션에 대한 기본값으로 사용하는 옵션 객체

timeline(sequence, {
  defaultOptions: { ease: "ease-in-out" },
});

 

 

특정 CSS 속성 값이 필요하기도 하다.

  • strokeDasharray
  • strokeDashoffset
  • pathLength
  • visibility
stroke-dasharray: 1;
stroke-dashoffset: 0; /* 애니메이션 값 변경: 0 → 1 */
visibility: visible;



 

 

☑️ 시퀀스(Sequence)

: 타임라인 시퀀스(순서)를 정의

  • 배열을 사용하여 설정한다. 
  • 애니메이션이 순서대로 재생된다.
const sequence = [
  ["nav", { x: 100 }, { duration: 1 }], // [1]
  ["nav li", { opacity: 1 }, { duration: 0.3, delay: stagger(0.1) }], // [2]
];

 

 

 

☑️ 앳(at)

: 애니메이션 타이밍 조정

  • 이전 애니메이션의 끝을 기준으로 조정 가능(+, -, <)
const sequence = [
  ["nav", { opacity: 1 }],
  ["nav", { x: 100 }, { at: "+0.5" }], // 이전 애니메이션 종료 0.5초 지난 후에 재생
  ["nav li", { opacity: 1 }, { at: "-0.2" }], // 이전 애니메이션 종료 0.2초 전에 재생
  ["nav li", { opacity: 1 }, { at: "<" }], // 이전 애니메이션과 동시에 재생
];

 

 

 

☑️ 레이블

: 시퀀스에 문자열을 전달하면 해당 시간에 레이블을 표시한 후, 나중에 앳(at)을 사용하여 참조할 수 있다.

const sequence = [
  ["nav", { opacity: 1 }, { duration: 2 }],
  "내비게이션 등장 시점",
  
  // ...
  
  ["nav li", { opacity: 1 }, { at: "내비게이션 등장 시점" }],
];

 

 


 

(3) inView()

: 요소가 윈도우 뷰포트 혹은 스크롤 가능한 상위 요소의 뷰포트에 진입할 때 호출

import { inView } from "motion"

inView(선택자 or DOM 요소, callback 함수);
  • 매개변수
    • 첫 번째 인수: CSS 선택자 또는 DOM 요소 혹은 집합(배열)
    • 두 번째 인수: 콜백(callback) 함수 (요소가 처음 뷰포트에 진입할 때 1회 실행)
  • 사용 예시
    • 스크롤해 볼 수 있는 요소에 애니메이션을 적용
    • 더 이상 보이지 않을 때 애니메이션을 비활성화
    • 로딩 속도가 느린 콘텐츠가 뷰포트에 진입하면 로딩
    • 비디오를 자동 시작 or 중지

 

 

☑️ 옵션

root

: 루트 요소가 제공된 경우, 대상 요소가 뷰포트에 있는지 감지

  • 기본 값:  윈도우 뷰포트
const carousel = document.querySelector(".carousel");

inView("#carousel li", callback, { root: carousel });

 

 

margin

: 루트 뷰포트에 적용할 CSS 마진으로 요소가 뷰포트에 바로 표시될 수 있도록 한다.

  • top / right / bottom / left 순으로 최대 4개의 값 허용
  • 기본 값 : 0
  • 양수 값: 뷰포트 경계를 루트 너머로 확장
  • 음수 값: 루트 안쪽을 경계로 축소
  • 설정 값은 픽셀 또는 백분율일 수 있다.
inView(element, callback, { margin: "0px 100px 0px 0px" });

 

 

amount

: 뷰포트 경계 내에 있어야 하는 대상 요소의 범위는 뷰포트에서 고려된다. 

  • 설정값: any(기본 값) / all 
  • 0~1 사이의 숫자 비율로 정의될 수 있다.
"any"(0)  ~  0.4  ~  "all" (1)

 

 

 

 

뷰포트 진출

  • 콜백 함수(callback)에서 반환된 함수는 요소가 뷰포트에 들어오거나 나갈 때 실행된다.
inView(
  element,
   (info) => {
     const controls = animate(info.target, { opacity: 1 });
    
     // 요소가 뷰포트를 벗어날 때 반환된 함수가 실행
     return (leaveInfo) => controls.stop();
   },
);

 

 

 

스크롤 가능한 요소 감지

  • inView() 함수는 설정된 요소가 뷰포트에 진입/진출할 때 감지한다.
  • 옵션에 루트(root) 요소를 설정하면, 스크롤 가능한 상위 요소의 뷰포트에 요소가 들어오거나 나갈 때를 감지할 수 있다.
const carousel = document.querySelector(".carousel");

inView("#carousel li", callback, { root: carousel });

 

 

 

뷰포트 감지 중지

  • inView() 함수는 실행 결과 감지를 중지하는 함수(stop())를 반환한다. 
const stop = inView(element, callback);

// 실행되면 더 이상 element 감지 중단
stop();

 

 


 

(4) scroll()

: 스크롤 애니메이션을 만드는 함수

import { scroll } from 'motion';

scroll(
  animate(DOM 요소, {애니메이션 옵션 객체})
);
  • 매개변수
    • 첫 번째 인수: CSS 선택자 또는 DOM 요소 혹은 집합(배열)
    • 두 번째 인수: 애니메이션 옵션 객체
  • animate(), timeline() 함수와 함께 사용
  • <video>, <canvas>, Three.js 등 모든 것을 애니메이션 할 수 있다.

 

 

☑️ 옵션

 

container
: 스크롤 위치를 추적할 스크롤 가능한 컨테이너 요소

  • 기본 값: Window
scroll(
 animation, 
  { 
   container: document.querySelector(".carousel") 
  }
);

 

 

 

target

: 컨테이너의 스크롤 가능한 영역

  • 뷰포트 내에서 진행 상황을 추적할 수 있도록 다른 요소를 설정할 수 있다.
scroll(
  animate(element, { opacity: [0, 1] }),
  {
    target: element,
    offset: ["start end", "end end"], // 대상이 컨테이너에 들어가는 경우
  }
);

 

 

axis
: 설정된 스크롤 축을 추적해 옵셋을 적용하고 애니메이션을 재생

  • 기본 값: "y"

 

offset
: 스크롤 진행률을 확인하는 데 사용할 스크롤 옵셋 목록

  • 기본 값: ["start start"`, `"end end"]

 

 

 

윈도우 스크롤

  • scroll() 함수에 전달된 애니메이션은 윈도우 뷰포트 스크롤 진행 상황에 따라 스크러빙(scrubing, 문지르는 행위) 된다.
import { scroll, animate } from "motion";

// 자동으로 스크러빙 할 애니메이션 또는 타임라인 설정
scroll(
  animate(".progress-bar", { scaleX: [0, 1] })
);

// 또는 스크롤 정보를 직접 사용할 수 있는 기능을 전달
const progress = document.querySelector(".progress");

scroll(({ y }) => {
  progress.innerHTML = y.progress.toFixed(2);
});

 

 

 

엘리먼트 스크롤

  • 스크롤 가능한 요소를 `container` 옵션으로 설정하면 해당 요소의 스크롤 진행률에 따라 스크러빙 된다.
import { scroll, animate } from "motion";

const carousel = document.querySelector("ul");

// 자동으로 스크러빙 할 애니메이션 또는 타임라인 설정
scroll(
  animate(".progress-bar", { scaleX: [0, 1] }),
  { container: carousel, axis: "x" },
);


// 자동으로 스크러빙 할 애니메이션 또는 타임라인 설정
const progress = document.querySelector(".progress");

scroll(
  ({ x }) => progress.innerHTML = x.progress.toFixed(2),
  { container: carousel, axis: "x" },
);

 

 

 

엘리먼트 포지션

  • 스크롤 진행률은 Window 객체 또는 `container` 옵션에 설정된 스크롤 가능한 요소의 뷰포트 범위의 위치를 추적하여 결정된다.
  • 스크롤 컨테이너 안에서 특정 요소가 이동할 때 추적하려면 `target` 옵션을 설정해야 한다.
import { scroll, animate } from "motion";

const secondItem = document.querySelectorAll("section div")[1];

const scrollOptions = {
  target: secondItem,
  offset: ["start end", "end end"],
};

// 자동으로 스크러빙 할 애니메이션 또는 타임라인 설정
scroll(
  animate(".progress-bar", { scaleX: [0, 1] }),
  scrollOptions,
);


// 자동으로 스크러빙 할 애니메이션 또는 타임라인 설정
const progress = document.querySelector(".progress");

scroll(
  ({ y }) => progress.innerHTML = y.progress.toFixed(2),
  scrollOptions,
);

 

 

 

스크롤 옵셋

  • 스크롤  offset 옵션은 키프레임을 스크롤 위치에 매핑
  • 스크롤 위치는 `container` 옵션에 설정된 스크롤 가능한 상위 요소의 뷰포트 기준으로 `target` 옵션에 설정된 요소의 스크롤 위치가 교집합 된 경우를 지정한다.
  • 각 offset은 대상(`target`)이 컨테이너(`container`)와 만나는 위치를 정의한다.
    ex) 대상의 시작이 컨테이너의 끝을 만나는 경우 "start end"로 표현
  • 옵션
    • 숫자: 시작 0 ~ 중앙 0.5 ~ 끝 1
    • 뷰포트: vh, vw 등
    • 네임: start, center, end
    • 픽셀
    • 백분율
const scrollOptions = {
  target: secondItem,
  offset: [
    // 대상(target)의 시작 위치가 컨테이너(container)의 스크롤 끝 위치에 도달하면 진행률 0%
    "start end", 
    // 대상의 끝 위치가 컨테이너의 스크롤 끝 위치에 도착하면 진행율 100%
    "end end"
  ],
};

 

 

 

함수 콜백

  • 스크롤 위치가 변경될 때마다 실행되는 콜백 함수를 `scroll()` 함수에 설정할 수 있다.
    👉 스크롤 위치를 통해 비디오 재생이나 Three.js와 같은 미디어에 애니메이션을 적용할 수 있습니다.

  •  콜백 함수에는 다음 정보를 포함하는 `info` 객체가 제공된다.
    • time : 스크롤 위치가 기록된 시간
    • x : 스크롤 x축 정보
    • y : 스크롤 y축 정보
  • 각 개별 축은 다음 데이터를 포함하는 객체이다.
    • current : 현재 스크롤 위치
    • offset : 스크롤 옵셋 값(픽셀)
    • progress : 옵셋에 따른 스크롤 진행률 값 (0 ~ 1)
    • scrollLength : 전체 스크롤 가능한 길이 (컨테이너 스크롤 가능 높이 - 컨테이너 높이)
    • velocity : 스크롤 속도
scroll(
  (info) => { /* ... */ },
  { 
    target: element, 
    offset: ScrollOffset.Enter,
  },
);

 

 


 

(5) stagger()

: 설정된 각 요소의 애니메이션 시간을 옵셋 설정 

  • 하나 이상의 요소로 애니메이션을 정의할 때, delay 옵션에 각 요소의 애니메이션을 지정된 시간만큼 지연시키는 함수인 stagger()를 설정할 수 있다.
  • 매개변수
    • 첫 번째 인수: 지연시킬 시간(초)
    • 두 번째 인수: 옵션
import { animate, stagger } from "motion";

animate(
  "li",
  { opacity: 1 },
  { delay: stagger(0.1) },
);

 

 

 

☑️ 옵션

  • stagger() 함수의 두번째 인수로 옵션을 허용

 

start

: 이후 지연 처리에 계산되는 초기 지연 시간

  • 기본 값: 0
stagger(0.1, { start: 0.2 }) 
// 0.2, 0.3, 0.4...

 

 

from

: 배열에서 어느 요소부터 스태거(stagger)를 설정할 지 지정

  • 인덱스를 지정하기 위해 "first", "center", "last" or 숫자 값을 설정할 수 있다.

 

 

easing

: 이징 함수에 따라 계산된 총 재생 시간에 걸쳐 계산된 이징을 분배하여, 각 애니메이션에서 지연 간 간격이 더 길어지거나 짧아진다.

  • 기본 값 : "linear"

 


 

ex 1) 무빙 애니메이션

import { animate } from 'motion';


function AnimateDemo() {
  const lollipopRef = useRef(null);

  const handleMoveAnimation = () => {
    const { current: element } = lollipopRef;

    animate(element, { y: -100, x: 400, rotate: 360 * 7 }, { duration: 4 });
  };

  return (
    <div className={S.component}>
      <button className={S.button} type="button" onClick={handleMoveAnimation}>
        무빙 애니메이션
      </button>

      <figure ref={lollipopRef} className={S.lollipop} />
    </div>
  );
}

 

 

 

ex 2) 진행률 애니메이션

import { animate } from 'motion';


function AnimateDemo() {

  const progressRef = useRef(null);

  const handleProgressAnimation = () => {
    const { current: element } = progressRef;

    const progressAnimation = (progress) => {
      const animationValue = Math.round(progress * 100) + '%';

      element.value = animationValue;
    };

    const animationOptions = {
      duration: 2,
      easing: 'ease-in-out',
    };

    animate(progressAnimation, animationOptions);
  };

  return (
      <div className={S.wrapper}>
        <button
          type="button"
          className={S.button}
          onClick={handleProgressAnimation}
        >
          진행률 애니메이션
        </button>
        <output ref={progressRef} className={S.output}>
          0%
        </output>
      </div>
  );
}

 

 

 

ex 3) 키프레임 애니메이션

import { animate } from 'motion';
import SoccorBall from '../components/SoccorBall';


function AnimateKeyframeDemo() {

  const containerRef = useRef(null); 
  const soccorBallRef = useRef(null);

  const handleMoveAnimate = () => {
    const { current: element } = soccorBallRef;

    animate(
      element,
      { x: [0, 400, 0], rotate: [0, 360, -360] },
      {
        duration: 1,
        easing: 'ease-out',
        repeat: 2,
        endDelay: 0.5
      }
    );
  };

  return (
    <div className={S.component} ref={containerRef}>
      <button className={S.button} type="button" onClick={handleMoveAnimate}>
        키프레임 애니메이션
      </button>

      <SoccorBall ref={soccorBallRef} size={60} />
    </div>
  );
}


// 축구공 SVG 컴포넌트 -----------------------
import { forwardRef } from 'react';

function _SoccorBall({ size = 40, color = '#450fbf', ...restProps }, ref) {

  return (
    <svg
      ref={ref}
      className={S.component}
      viewBox="-105 -105 210 210"
      width={size}
      height={size}
      {...restProps}
    >
    // ...
    </svg>
  );
}

_SoccorBall.displayName = 'SoccorBall';

const SoccorBall = forwardRef(_SoccorBall);

export default SoccorBall;

 

 


ex 4) 타임라인 애니메이션

import { timeline } from 'motion';

function SVGPathTimeline({ size = 60 }) {
  const svgCircleRef = useRef(null);
  const svgPathRef = useRef(null);

  const handleTimelineAnimate = () => {
    const circleElement = svgCircleRef.current;
    const pathElement = svgPathRef.current;

    const sequence = [
      [
        circleElement,
        { strokeDashoffset: [1, 0], visibility: 'visible' },
        { duration: 0.4, easing: 'ease-out' },
      ],
      [
        pathElement,
        { strokeDashoffset: [1, 0], visibility: 'visible' },
        { duration: 0.2, easing: 'ease-in-out', at: '+0.1' },
      ],
    ];

    timeline(sequence);
  };

  
  return (
    <div className={S.component}>
      <button
        className={S.button}
        type="button"
        onClick={handleTimelineAnimate}
      >
        타임라인 애니메이션
      </button>

      <svg width={size} height={size} viewBox="0 0 200 200">
        <circle
          ref={svgCircleRef}
          className={S.circle}
          cx="100"
          cy="100"
          r="80"
          pathLength="1"
        />
        <path
          ref={svgPathRef}
          className={S.path}
          d="M 54 107.5 L 88 138.5 L 144.5 67.5"
          pathLength="1"
        />
      </svg>
    </div>
  );
}

 

 

 

ex 5) SVG가 그려지는 애니메이션

import { timeline } from 'motion';
import { useRef } from 'react';
import CircleLine from './CircleLine';

function PracticeSVGPathAnimation() {
  const svgRef = useRef(null);

  const handleSVGPathAnimation = async () => {
    const { current: el } = svgRef;

    const [circle1, circle2] = Array.from(el.querySelectorAll('circle'));
    const line = el.querySelector('line');

    const animationControls = timeline(
      [
        [circle1, { strokeDashoffset: [1, 0], visibility: 'visible' }],
        [line, { strokeDashoffset: [1, 0], visibility: 'visible' }],
        [circle2, { strokeDashoffset: [1, 0], visibility: 'visible' }],
      ],
      {
        duration: 1.45,
        easing: 'ease-in-out',
      }
    );

    await animationControls.finished;

    // 타임라인 애니메이션 종료 이후 정리
    // Array.from(el.children).forEach((child) => {
    //   child.style.visibility = 'hidden';
    // });
  };

  return (
    <>
      <button
        type="button"
        className={S.button}
        onClick={handleSVGPathAnimation}
      >
        SVG 패스 애니메이션
      </button>

      <div className={S.component}>
        <CircleLine forwardRef={svgRef} />
      </div>
    </>
  );
}


// 원 SVG 컴포넌트 --------------------------------
import { any, exact, number, string } from 'prop-types';

function CircleLine({
  forwardRef = { current: null },
  strokeColor = '#4729B4',
  strokeWidth = 6,
}) {

  return (
    <svg
      ref={forwardRef}
      className={S.component}
      width={212}
      height={41}
      viewBox="0 0 210 41"
      fill="none"
    >
      <circle
        cx="20.5"
        cy="20.5"
        r="17.5"
        stroke={strokeColor}
        strokeDasharray={1}
        strokeDashoffset={0}
        strokeWidth={strokeWidth}
        pathLength={1}
      />
      <line
        x1={36}
        y1={20}
        x2={173}
        y2={20}
        stroke={strokeColor}
        strokeDasharray={1}
        strokeDashoffset={0}
        strokeWidth={strokeWidth}
        pathLength={1}
      />
      <circle
        cx="189.5"
        cy="20.5"
        r="17.5"
        stroke={strokeColor}
        strokeDasharray={1}
        strokeDashoffset={0}
        strokeWidth={strokeWidth}
        pathLength={1}
        style={{ transform: 'rotateX(180deg) rotateY(180deg)' }}
      />
    </svg>
  );
}

export default CircleLine;

 

 

 

ex 6) 인뷰(inView) 애니메이션

import { inView, timeline } from 'motion';

function ScrollTriggerItem({ item }) {
  const pRef = useRef(null);

  const setScrollTrigger = (element) => {

    if (element) {
      // 문서에 inView() 함수를 적용할 요소가 있을 경우

      // 스크롤 트리거 설정
      inView(element, (/* info */ { target }) => {

        // 타임라인 애니메이션 설정
        const animation = timeline(
          [
            [target, { opacity: [0, 1] }, { duration: 0.6 }],
            [
              pRef.current,
              { opacity: [0, 1], y: [20, 0] },
              { duration: 0.4, at: '+0.8' },
            ],
          ],
          { easing: 'ease-out' }
        );

        // inView() 함수에 설정된 콜백 함수가 반환하는 함수는
        // 엘리먼트가 뷰포트를 벗어났을 때 실행
        return () => {
          animation.stop();
        };
      });
    }
  };

  return (
    <article
      ref={setScrollTrigger}
      className={S.component}
      style={{ background: `url(${item.image}) no-repeat center / cover` }}
    >
      <p ref={pRef} className={S.text}>
        {item.text}
      </p>
    </article>
  );
}

 

 

 

ex 7) 스크롤 진행률 애니메이션

  • CSS의 `transform: scaleX(0)` 사용
    👉 진행률을 기반으로 바의 너비를 조절하기 위해
function ScrollAnimation() {
  const [images] = useState(IMAGES);

  return (
    <section className={S.component}>
      <h2 className="sr-only">스크롤 트리거 애니메이션</h2>
      <ProgressBar containerSelector={`.${S.component} ul`} />
      {/* S.component 클래스 이름을 가진 아래의 ul 태그를 선택 */}
      
      <ul>
        {images.map((item) => (
          <ScrollItem key={item.id} item={item} />
        ))}
      </ul>
    </section>
  );
}

export default ScrollAnimation;


// 진행률 -----------------------------
function ProgressBar({ containerSelector = null, axis = 'y' }) {
  // containerSelector: 스크롤 이벤트를 감지할 컨테이너를 선택하기 위한 CSS 선택자
  // axis: 수직 스크롤

  // 진행률 바의 DOM 요소를 참조
  const progressBarRef = useRef(null);

  // 진행률 값(%)을 표시하는 output 요소를 참조
  const outputRef = useRef(null);


  // 진행률 바를 설정하고 업데이트하기 위한 함수
  // - 사용자 액션 이벤트 X
  // - 마운트 시점에 실행될 콜백 함수 : ref callback
  const setProgressBar = () => {
    // DOM에서 스크롤을 감지할 컨테이너를 선택
    const container = document.querySelector(containerSelector);

    // 스크롤 애니메이션에 필요한 옵션
    const scrollOptions = { container, axis };

    // 스크롤 애니메이션
    scroll(({ y: { progress } }) => {
      // 현재 스크롤 위치에 대한 진행률을 인자로 받음

      // 실제 DOM 요소를 참조
      const progressBar = progressBarRef.current;
      const output = outputRef.current;

      // progressBar와 output이 존재할 경우에만 다음 작업을 수행
      if (progressBar && output) {
        // 진행률 바의 너비 조절
        progressBarRef.current.style.transform = `scaleX(${progress})`;
        
        // output의 값을 현재 스크롤의 진행률(퍼센트)로 설정
        output.value = (progress * 100).toFixed(0) + '%';
      }
    }, scrollOptions);
  };

  return (
    // div가 마운트될 때, setProgressBar 함수가 호출됨
    <div ref={setProgressBar}>
      <div ref={progressBarRef} className={S.progress} />
      <output ref={outputRef} className={S.output}>
        0%
      </output>
    </div>
  );
}

export default ProgressBar;

 

 

 

ex 8) 스태커 애니메이션(축구공)

import { animate, stagger } from 'motion';
import { useRef, useState } from 'react';
import SoccorBall from './components/SoccorBall';
import S from './style.module.css';


function MotionOneStagger() {
  // 애니메이션할 공의 개수를 정의
  const [balls] = useState(Array(6).fill(null));

  // DOM 요소를 저장하는 데 사용
  const soccorBallsRef = useRef(null); 

  // soccorBallsRef.current가 존재하지 않을 경우, 새로운 Map 객체를 생성하여 반환
  const getMap = () => {
    if (!soccorBallsRef.current) {
      soccorBallsRef.current = new Map();
    }

    return soccorBallsRef.current;
  };


  // 애니메이션을 시작하는 함수
  const handleAnimateBalls = () => {
    // Map 활용 예시 코드 (공식 문서에서 기술하는 방법)
    const map = getMap();
    const mapArray = Array.from(map.values());

    if (mapArray.length > 0) {

      animate(
        mapArray,
        { x: [0, 400, 0], rotate: [0, 360, -360] },
        {
          delay: stagger(0.3),
          duration: 2,
        }
      );
    }
  };

  // SoccorBall 컴포넌트가 마운트되거나 언마운트될 때 호출
  const mountedCallback = (index, el) => {
    const map = getMap();

    if (el) {
      // el이 존재하면 index와 el 쌍을 Map에 추가
      map.set(index, el);

    } else {
      // el이 없으면 Map에서 해당 index를 삭제
      map.delete(index);
    }
    // 참조 객체의 current 값에 담긴 객체 (얼마든지 수정)
    // const soccorBalls = soccorBallsRef.current;
    // soccorBalls.push(soccorBallElement);
  };

  return (
    <main className={S.component}>
      <h1 className={S.headline} lang="en">
        stagger()
      </h1>
      <div className={S.description}>
        <p>
          Motion One 라이브러리를 사용해 실제 DOM 노드에 애니메이션을
          적용합니다.
        </p>
        <p>
          자세한 사용법은{' '}
          <a
            href="https://motion.dev/docs/stagger"
            rel="noreferrer noopener"
            target="_blank"
          >
            stagger()
          </a>
          문서를 참고합니다.
        </p>
      </div>

      <div className={S.description}>
        <p>
          사커볼이 화면 벽면에 부딫힌 후, 다시 돌아오도록 애니메이션을
          설정합니다.
        </p>
      </div>

      <button className={S.button} type="button" onClick={handleAnimateBalls}>
        스태거 애니메이션
      </button>

      <div className={S.balls}>
        {balls.map((color, index) => {
          return (
            <SoccorBall ref={mountedCallback.bind(null, index)} key={index} />
            // SoccorBall이 DOM에 마운트될 때 mountedCallback 함수를 호출하여 해당 요소의 참조를 Map 객체에 저장
            // - mountedCallback 함수의 this 컨텍스트를 null로 설정하고, 첫 번째 인자로 index를 전달하는 새로운 함수를 생성
            // - SoccorBall 요소가 마운트될 때 호출된다.
          );
        })}
      </div>
    </main>
  );
}

export default MotionOneStagger;


// 축구공 SVG ----------------------------------------

function SoccorBall({ size = 40, color = '#450fbf', ...restProps }, ref) {
  return (
    <svg
      ref={ref}
      className={S.component}
      viewBox="-105 -105 210 210"
      width={size}
      height={size}
      {...restProps}
    >
    //...
    </svg>
  );
}

export default forwardRef(SoccorBall);

 

 

 

 


 

 

Vanilla-tilt.js

Tilt change event The tilt change event will output the x,y & percentages of tilting. let eventBox = document.querySelector("#box-event"); let outputContainer = document.querySelector(".output"); VanillaTilt.init(eventBox); eventBox.addEventListener("tiltC

micku7zu.github.io

 

 

Motion One - A modern JavaScript animation library

Motion One is built on native browser APIs for a tiny filesize and superfast performance. It uses hardware acceleration for smooth and eco-friendly animations.

motion.dev

 

 

Motion One | Notion

네이티브 브라우저 API를 기반으로 하는 최신 웹 애니메이션 라이브러리

euid.notion.site