내가 Excalidraw용 'Magic Move' 애니메이션 엔진을 처음부터 만든 방법 (출판)
Source: Dev.to
목표
저는 시스템 아키텍처를 스케치할 때 Excalidraw를 사랑합니다. 하지만 스케치는 정적입니다. 로드 밸런서를 통과하는 패킷이나 데이터베이스 샤드가 분할되는 과정을 보여주고 싶을 때마다 손을 허둥대며 설명하거나 10개의 서로 다른 슬라이드를 만들어야 했습니다.
저는 “스케치 로직, 모션 내보내기” 를 할 수 있기를 원했습니다.
After Effects 같은 타임라인 편집기는 원하지 않았습니다. 단순한 다이어그램에 비해 너무 과한 작업이니까요.
저는 “키 없는 애니메이션” 을 원했습니다:
- 프레임 1 (시작 상태)을 그린다.
- 이를 프레임 2 로 복제한다.
- 요소들을 새로운 위치로 이동한다.
- 엔진이 자동으로 전환을 계산한다.
저는 이 엔진을 Next.js, Excalidraw, 그리고 Framer Motion 으로 만들었습니다. 아래는 구현 로직에 대한 기술적인 깊이 있는 탐구입니다.
1. 핵심 로직: 상태 차이점 찾기 (Diffing)
가장 어려운 부분은 애니메이션 루프가 아니라 차이점 찾기 입니다. 프레임 A 에서 프레임 B 로 이동할 때, 우리는 요소들을 안정적인 ID 로 식별하고 세 가지 버킷 중 하나에 분류합니다:
- Stable: 두 프레임 모두에 존재하는 요소 (형태 변환/이동 필요).
- Entering: B에는 존재하지만 A에는 없는 요소 (페이드 인 필요).
- Exiting: A에는 존재하지만 B에는 없는 요소 (페이드 아웃 필요).
아래는 요소들을 효율적으로 매핑하는 categorizeTransition 유틸리티입니다:
// Simplified logic from src/utils/editor/transition-logic.ts
export function categorizeTransition(prevElements, currElements) {
const stable = [];
const morphed = [];
const entering = [];
const exiting = [];
const prevMap = new Map(prevElements.map(e => [e.id, e]));
const currMap = new Map(currElements.map(e => [e.id, e]));
// 1. Find Morphs (Stable) & Entering
currElements.forEach(curr => {
if (prevMap.has(curr.id)) {
const prev = prevMap.get(curr.id);
// Separate "Stable" (identical) from "Morphed" (changed)
// to optimize the render loop
if (areVisuallyIdentical(prev, curr)) {
stable.push({ key: curr.id, element: curr });
} else {
morphed.push({ key: curr.id, start: prev, end: curr });
}
} else {
entering.push({ key: curr.id, end: curr });
}
});
// 2. Find Exiting
prevElements.forEach(prev => {
if (!currMap.has(prev.id)) {
exiting.push({ key: prev.id, start: prev });
}
});
return { stable, morphed, entering, exiting };
}
2. 속성 보간 (Interpolating Properties)
Morphed 요소에 대해서는 주어진 progress (0.0 → 1.0) 에서 중간 상태를 계산해야 합니다.
- 숫자 (x, y, width): 선형 보간이 충분합니다.
- 색상 (strokeColor): Hex 를 RGBA 로 변환하고 각 채널을 보간한 뒤 다시 변환합니다.
- 각도: “최단 경로” 보간을 사용합니다.
예를 들어 객체가 10° 에서 350° 로 회전한다면, 선형 보간은 긴 경로를 따라가게 됩니다. 아래 헬퍼는 최단 방향을 찾아줍니다:
// src/utils/smart-animation.ts
const angleProgress = (oldAngle, newAngle, progress) => {
let diff = newAngle - oldAngle;
// Normalize to -π to +π to find shortest direction
while (diff > Math.PI) diff -= 2 * Math.PI;
while (diff {
return 1 - Math.pow(1 - t, 4);
};
이렇게 하면 큰 아키텍처 블록이 유령처럼 미끄러지는 대신 무게감 있게 “쿵” 하고 제자리에 자리 잡게 됩니다.
다음 단계는?
현재 진행 중인 작업은 다음과 같습니다:
- 하위 단계 애니메이션: 하나의 프레임 안에서 불릿 포인트를 클릭해 순차적으로 표시할 수 있게 하기.
- MP4 로 내보내기: 캔버스 스트림을 직접 비디오 파일로 기록하기.
프로젝트는 라이브이며, 개발자들이 더 잘 소통하도록 돕기 위해 만들었습니다.
여기서 체험해 보세요:
Free Stripe Promotion Code: postara
접근 방식에 대한 의견을 알려 주세요!