AndChill 개발일지(7) - 영화카드 뽑기(animation과의 전쟁)

dali·2024년 9월 16일
0

And Chill 개발 기록

목록 보기
7/9
post-thumbnail

기획때부터 상상만 하던 기능을 완성하고야 말았습니다..

이런 복잡한 애니메이션은 구현할 때마다 느끼는거지만 정말 꼼꼼해야 하는 것 같습니다. 0.X 초만 차이가 나더라도 바로 어색한 애니메이션이 되어버리기 때문입니다. keyframesetTimeout 은 원래 animation time 을 반영해서 적용해도 이미지 렌더링, 웹 성능 등의 이유 때문인지 0.1~0.3 초 정도 차이가 나서 애를 조금 먹었습니다 ㅠ (하지만 해냈죠?😈)


카드 뽑는데 어떤 애니메이션이 필요할까?

위 움짤에서처럼 작동하려면 4가지 애니메이션들을 조합해야 합니다.

  • Shuffle - 카드 덱 섞기
  • Spread - 카드 덱 펼치기
  • Stack - 카드 덱 모으기
  • Flip - 단일 카드 뒤집기

Shuffle Animation

덱의 6장의 카드가 입체감을 위해 position이 서로 조금씩 다르게 위치하고, 섞이는 방향, 순서, 섞은 후 이동할 위치도 각각 다르므로 카드마다 애니메이션을 다르게 지정해주어야 합니다.

1번 카드에 적용할 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가지 경우에 따라 시퀀스 진행 순서가 조금 다릅니다. 이에 따라 각 애니메이션이 시행될 시간도 다르게 적용되어야 합니다.

  • 카드가 모두 비공개인 상태
    버튼 비활성화모으기셔플펼치기버튼 활성화
  • 공개된 카드가 1개 이상인 상태
    버튼 비활성화공개된 카드 뒤집기모으기셔플펼치기버튼 활성화
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'}`
}

0개의 댓글

관련 채용 정보