✅ 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
: 전체 애니메이션에 사용되는 이징 리스트
- 기본 이징 함수 : linear, ease, ease-in, ease-out, ease-in-out
- 기본 값: ease
- 큐빅 베지어 곡선 : [0.17, 0.67, 0.83, 0.67]
- 스탭 이징 : steps(2, start)
- 커스텀 이징 : JavaScript 이징 함수
- 애니메이션 전체에 적용되는 이징 설정
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축 정보
- time : 스크롤 위치가 기록된 시간
- 각 개별 축은 다음 데이터를 포함하는 객체이다.
- current : 현재 스크롤 위치
- offset : 스크롤 옵셋 값(픽셀)
- progress : 옵셋에 따른 스크롤 진행률 값 (0 ~ 1)
- scrollLength : 전체 스크롤 가능한 길이 (컨테이너 스크롤 가능 높이 - 컨테이너 높이)
- velocity : 스크롤 속도
- current : 현재 스크롤 위치
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);