기획때부터 상상만 하던 기능을 완성하고야 말았습니다..
이런 복잡한 애니메이션은 구현할 때마다 느끼는거지만 정말 꼼꼼해야 하는 것 같습니다. 0.X 초만 차이가 나더라도 바로 어색한 애니메이션이 되어버리기 때문입니다. keyframe
과 setTimeout
은 원래 animation time 을 반영해서 적용해도 이미지 렌더링, 웹 성능 등의 이유 때문인지 0.1~0.3 초 정도 차이가 나서 애를 조금 먹었습니다 ㅠ (하지만 해냈죠?😈)
위 움짤에서처럼 작동하려면 4가지 애니메이션들을 조합해야 합니다.
Shuffle
- 카드 덱 섞기Spread
- 카드 덱 펼치기Stack
- 카드 덱 모으기Flip
- 단일 카드 뒤집기Shuffle
Animation1번 카드에 적용할 shuffle1
애니메이션
export const shuffle1 = keyframes`
0% {z-index: 6; transform: translate(0%, 0%);}
5% {z-index: 6; transform: translate(-65%, 0%) skewY(4deg);}
6% {z-index: 0;}
10% {z-index: 0; transform : translate(0%, 0%);}
20% {z-index: 1;}
30% {z-index: 2;}
40% {z-index: 3;}
50% {z-index: 4;}
60% {z-index: 5;}
100%{z-index: 6;}
`;
2번 카드에 적용할 shuffle2
애니메이션
export const shuffle2 = keyframes`
0% {z-index: 5;}
10% {z-index: 6; transform: translate(0%, 0%);}
15% {z-index: 6; transform: translate(65%, -10%) skewY(-4deg);}
16% {z-index: 0;}
20% {z-index: 0; transform : translate(0%, 0%);}
30% {z-index: 1;}
40% {z-index: 2;}
50% {z-index: 3;}
60% {z-index: 4;}
100%{z-index: 5;}
`;
. . .
이렇게 6번 카드까지 적용하면 shuffle 구현 ㅎ🤢
Spread
Animation모여있는 위치에서 각자의 지정된 행, 열의 위치로 펼쳐져야 하므로 각 카드에서 선언된 css 변수인 row
, col
, index
을 사용해 적절한 위치로 펼쳐지게 하였습니다.
3번 카드 예시
&.card1 {
z-index: 4;
...
--index: 3;
--row: 0;
--col: 2;
}
spreadAnimation
export const spreadAnimation = keyframes`
0% {
transform: translateY(0);
}
100% {
transform:
translateY(calc(var(--row) * 320px - 140px - var(--index) * 4px))
translateX(calc(var(--col) * 220px - 204px - var(--index) * 5px));
}
`;
Stack
Animation모으기 애니메이션은 간단하게 펼치기 애니메이션을 반대로 진행해주면 되겠죠~?
stackAnimation
export const stackAnimation = keyframes`
0% {
transform:
translateY(calc(var(--row) * 320px - 140px - var(--index) * 4px))
translateX(calc(var(--col) * 220px - 204px - var(--index) * 5px));
}
100% {
transform: translateY(0) translateX(0);
}
`;
Flip
Animation카드 뒤집기 모션은 각 카드가 다르게 독립적으로 동작해야 하므로 카드가 뒷면으로 뒤집혀져 있을 때, 클릭하면 180도 돌려주는 방식으로 간단하게 구현했습니다.
Card: styled.div<{$flipped?: boolean; $isDisabled?: boolean; ... }>`
...
.card-container {
...
transition: transform 0.5s;
transform-style: preserve-3d;
cursor: ${(props) => (props.$isDisabled ? 'default' : 'pointer')};
transform: rotateY(${(props) => (props.$flipped ? '0deg' : '180deg')});
}
카드들은 기본적으로 모두 펼쳐진 상태에서 시작되고, 셔플 버튼을 클릭하면 애니메이션 시퀀스가 시작됩니다! 이 때 2가지 경우에 따라 시퀀스 진행 순서가 조금 다릅니다. 이에 따라 각 애니메이션이 시행될 시간도 다르게 적용되어야 합니다.
버튼 비활성화
→모으기
→셔플
→펼치기
→버튼 활성화
버튼 비활성화
→공개된 카드 뒤집기
→모으기
→셔플
→펼치기
→버튼 활성화
const handleAnimationSequence = () => {
setClicked(true); // click 상태가 true 면 랜덤 영화 데이터 재호출
setIsAllNotFlipped(flipped.every((flip) => !flip)); // 공개된 카드 있는지 확인
setIsDisabled(true); // 버튼 비활성화
setFlipped(Array(6).fill(false)); // 모든 카드 비공개 상태로 만들기
const DELAY = isAllNotFlipped ? 3700 : 4280;
const STACK_TIMEOUT = isAllNotFlipped ? 0 : 580;
const SHUFFLE_TIMEOUT = isAllNotFlipped ? 600 : 1180;
const SPREAD_TIMEOUT = isAllNotFlipped ? 800 : 1380;
setTimeout(() => setIsDisabled(false), DELAY); // 시퀀스 진행이 끝나면 다시 버튼 활성화
setTimeout(() => handleStack(), STACK_TIMEOUT);
setTimeout(() => handleShuffle(), SHUFFLE_TIMEOUT);
setTimeout(() => handleSpread(), SPREAD_TIMEOUT);
};
카드 섞기 시퀀스
const handleShuffle = () => {
setPage(Math.floor(Math.random() * 500) + 1); // 랜덤 카드 페이지 선택
setAnimate(true); // 셔플 애니메이션 진행
setTimeout(() => setAnimate(false), SHUFFLE_TIME);
setFlipped(Array(6).fill(false));
};
카드 펼치기 시퀀스
const handleSpread = () => {
setSpread(true);
setStack(false);
};
카드 모으기 시퀀스
const handleStack = () => {
setSpread(false);
setStack(true);
};
위 함수들을 조합해서 적절한 time 동안 실행 및 delay를 시킨 다음, 클래스명에 각 상태를 대입하여 애니메이션을 실행합니다.
className={
`card${i + 1} ${animate && 'animate'} ${spread && 'spread'} ${stack && 'stack'}`
}