// 애니메이션, Animation
- 웹 애플리케이션에서 동적인 작용으로 부드러운 전환, 변화 또는 상호작용을 구현하는 방법
- CSS animation , 외부 라이브러리 animation 등을 사용해서 구현한다.
// CSS animation
- 특정한 애니메이션, 즉 일련의 단계를 css로 정의한다.
- DOM에서 요소가 삭제되면 스타일을 적용할 수 없기 때문에 특정 요소를 나타나게 할 수는 있지만 사라지게 할 수 없다.
- 복잡한 애니메이션은 구현이 어렵다.
@keyframes animation명 {
from {}
to{}
}
@keyframes animation명 {
0% {}
100%{}
}
- 원하는 단계별로 나누어 정의할 수있다.
css 선택자 {
animation: 애니메이션명
}
- forwards를 추가하면 애니메이션 실행이 끝난 후 최종상태를 유지한다.
.modal {
top: 10%;
border-radius: 6px;
padding: 1.5rem;
width: 30rem;
max-width: 90%;
z-index: 10;
animation: slide-up-fade-in 0.3s ease-out forwards;
}
@keyframes slide-up-fade-in {
0% {
transform: translateY(30px);
opacity: 0;
}
100% {
transform: translateY(0px);
opacity: 1;
}
}
// 프레이머 모션, Framer Motion
- 복잡한 애니메이션을 구현해주는 라이브러리
- 공식 사이트: https://www.framer.com/motion/
- 설치
npm install framer-motion
- motion.html 요소로 사용한다.
<motion.html요소 animation={{}} transition={{}} initial:{{}} exit={{}} whileHover={{}} />
- animation : animation을 적용한 값이 변할 때마다 애니메이션을 재실행한다.
- 애니메이션화 하는 속성에 대한 값으로 배열(key frame)을 넣을 수도 있다. - initial : 해당 요소가 DOM에 추가된 직후 재생될 애니메이션의 초기 상태를 정의한다.
- exit : 해당 요소가 DOM에서 삭제될 때 재생될 애니메이션을 정의한다.
- 대신 해당 요소를 <AnimatePresence>로 감싸야 한다. - whileHover : hover일 때만 실행할 애니메이션을 정의한다.
<motion.html요소 variants={{ style명: { }, }} animate="style명" />
- variants
- 변수 선언과 비슷한 용도로 사용되어 재사용이 용이하다.
- 자식 컴포넌트의 애니메이션을 트리거할 수도 있다.
> 부모 컴포넌트에서의 style명과 동일하게 자식 컴포넌트에서 variants를 선언하면, initial, animate등 설정은 따로 해주지 않아도 자동으로 부모 컴포넌트를 따라간다.
> 부모 컴포넌트와 다르게 사용하고 싶을 경우 variants를 사용할 수 없고 직접 style 객체로 정의해야 한다.
<motion.html요소
drag
dragConstraints={}
dragSnapToOrigin
dragElastic={}
/>
- drag: 요소를 드래그할 수 있게 한다.
- drag="x" > x축 내에서만 드래그할 수 있다.
- drag="y" > y축 내에서만 드래그할 수 있다. - dragConstraints: 드래그 범위를 제한할 수 있다.
~ { top: 0, bottom: 0, left: 0, right: 0 }
- ref로 드래그 범위를 제할할 수도 있다. - dragSnapToOrigin: 드래그 후 원래 위치로 돌아가게 한다.
- dragElastic : 원래 위치로 돌아가려는 탄성
※ styled-components와 framer 모션을 함께 사용하려면 styled-components 사용 규칙이 바뀐다.
const Styled컴포넌트명 = styled(motion.HTML요소명)`
`;
// Staggering
- 모든 리스트 항목이 같은 시점에 애니메이션이 실행되는 것이 아니라 차례대로 실행되도록 한다.
<motion.ul variants={{ style명: { transition: { staggerChildren: 0.05 } } }}>
</motion.ul>
import { useContext, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { ChallengesContext } from '../store/challenges-context.jsx';
import Modal from './Modal.jsx';
import images from '../assets/images.js';
export default function NewChallenge({ onDone }) {
const title = useRef();
const description = useRef();
const deadline = useRef();
const [selectedImage, setSelectedImage] = useState(null);
const { addChallenge } = useContext(ChallengesContext);
function handleSelectImage(image) {
setSelectedImage(image);
}
function handleSubmit(event) {
event.preventDefault();
const challenge = {
title: title.current.value,
description: description.current.value,
deadline: deadline.current.value,
image: selectedImage,
};
if (
!challenge.title.trim() ||
!challenge.description.trim() ||
!challenge.deadline.trim() ||
!challenge.image
) {
return;
}
onDone();
addChallenge(challenge);
}
return (
<Modal title="New Challenge" onClose={onDone}>
<form id="new-challenge" onSubmit={handleSubmit}>
<p>
<label htmlFor="title">Title</label>
<input ref={title} type="text" name="title" id="title" />
</p>
<p>
<label htmlFor="description">Description</label>
<textarea ref={description} name="description" id="description" />
</p>
<p>
<label htmlFor="deadline">Deadline</label>
<input ref={deadline} type="date" name="deadline" id="deadline" />
</p>
<motion.ul
id="new-challenge-images"
variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
>
{images.map((image) => (
<motion.li
variants={{
hidden: { opacity: 0, scale: 0.5 },
visible: { opacity: 1, scale: 1 },
}}
exit={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring' }}
key={image.alt}
onClick={() => handleSelectImage(image)}
className={selectedImage === image ? 'selected' : undefined}
>
<img {...image} />
</motion.li>
))}
</motion.ul>
<p className="new-challenge-actions">
<button type="button" onClick={onDone}>
Cancel
</button>
<button>Add Challenge</button>
</p>
</form>
</Modal>
);
}
import { createPortal } from 'react-dom';
import { motion } from 'framer-motion';
export default function Modal({ title, children, onClose }) {
//const hiddenAnimationState = { opacity: 0, y: 30 };
return createPortal(
<>
<div className="backdrop" onClick={onClose} />
<motion.dialog
variants={{
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate="visible"
exit="hidden"
open
className="modal"
>
<h2>{title}</h2>
{children}
</motion.dialog>
</>,
document.getElementById('modal'),
);
}
// useAnimate
- 동적으로 애니메이션을 추가할 때 사용한다.
const [scope, animate] = useAnimate();
- scope : ref
- animate : 함수
animate(
'css 선택자',
{ style 객체 },
{ transition 설정 },
);
if (
!challenge.title.trim() ||
!challenge.description.trim() ||
!challenge.deadline.trim() ||
!challenge.image
) {
animate(
'input, textarea',
{ x: [-10, 0, 10, 0], borderColor: 'red' },
{ duration: 0.1 },
);
return;
}
// 스크롤 기반 애니메이션
- useScroll, useTransform 을 사용한다.
~ useScroll : scroll event를 감지하여 사용자가 스크롤을 얼마나 내렸는지 알려준다.
~ useTransform : animation에서 사용할 수 있는 값으로 변환해준다.
const { scrollY, scrollYProgress } = useScroll();
- scrollY: 사용자가 스크롤을 얼마나 내렸는지 px을 반환해준다.
- scrollYProgress: 사용자가 스크롤을 얼마나 내렸는지 0(최상단)부터 1(최하단)까지의 상대적은 값을 반환해준다.
const transform변수명 = useTransform(스크롤변수, [중단점], [설정하려는 값]);
- 스크롤변수: useScroll을 통해 받아온 변수
- 중단점 : 변화가 일어나야 하는 지점 배열
- scrollY와 같은 절대 변수일 경우 변화가 일어날 픽셀 설정 - 설정하려는 값 : 중단점마다 변화될 값
<motion.html요소 style={{ style속성: transform변수명 }}> </motion.html요소>
- animate가 아닌 style property를 사용한다.
import { Link } from 'react-router-dom';
import { motion, useScroll, useTransform } from 'framer-motion';
import cityImg from '../assets/city.jpg';
import heroImg from '../assets/hero.png';
export default function WelcomePage() {
const { scrollY } = useScroll();
const opacityCity = useTransform(scrollY, [0, 200, 500], [1, 0.5, 0]);
return (
<>
<header id="welcome-header">
<motion.div id="welcome-header-content">
<h1>Ready for a challenge?</h1>
<Link id="cta-link" to="/challenges">
Get Started
</Link>
</motion.div>
<motion.img
style={{ opacity: opacityCity }}
src={cityImg}
alt="A city skyline touched by sunlight"
id="city-image"
/>
<motion.img
src={heroImg}
alt="A superhero wearing a cape"
id="hero-image"
/>
</header>
<main id="welcome-content">
<section>
<h2>There's never been a better time.</h2>
<p>
With our platform, you can set, track, and conquer challenges at
your own pace. Whether it's personal growth, professional
achievements, or just for fun, we've got you covered.
</p>
</section>
<section>
<h2>Why Challenge Yourself?</h2>
<p>
Challenges provide a framework for growth. They push boundaries,
test limits, and result in genuine progress. Here, we believe
everyone has untapped potential, waiting to be unlocked.
</p>
</section>
<section>
<h2>Features</h2>
<ul>
<li>Custom challenge creation: Set the rules, define your pace.</li>
<li>
Track your progress: See your growth over time with our analytics
tools.
</li>
<li>
Community Support: Join our community and get motivated by peers.
</li>
</ul>
</section>
<section>
<h2>Join Thousands Embracing The Challenge</h2>
<p>
“I never realized what I was capable of until I set my first
challenge here. It's been a transformative experience!” - Alex
P.
</p>
{/* You can add more testimonials or even a carousel for multiple testimonials */}
</section>
</main>
</>
);
}
// motionValue
- 움직임을 추적하여 값을 얻을 수 있다.
const 변수명 = useMotionValue(0);
useMotionValueEvent(변수명, 'change', (coordinate) => {});
<motion.HTML요소 style={{ 변수명 }} />
- 변수의 움직임에 따른 값을 얻을 수 있다.
'클라이언트 > React' 카테고리의 다른 글
[리액트(React)] Tanstack Query(React Query) Devtools (0) | 2024.03.22 |
---|---|
[리액트(React)] 유닛 테스트(Unit Test) (8) | 2024.03.07 |
[리액트(React)] Tanstack Query(React Query) (0) | 2024.02.24 |
[리액트(React)] 배포(Deploying) (0) | 2024.02.23 |
[리액트(React)] 인증(Authentication) (0) | 2024.02.22 |