요약
클립에 애니메이션을 적용할 때는 크기 변환을 사용하세요. 하위 요소의 크기를 반대로 조정하여 애니메이션 중에 하위 요소가 늘어나거나 왜곡되지 않도록 할 수 있습니다.
이전에 성능 기준에 맞는 시차 효과와 무한 스크롤러를 만드는 방법에 관한 업데이트를 게시했습니다. 이 게시물에서는 성능이 우수한 클립 애니메이션을 만들기 위해 필요한 사항을 살펴봅니다. 데모를 보려면 샘플 UI 요소 GitHub 저장소를 확인하세요.
확장되는 메뉴를 예로 들 수 있습니다.
빌드하는 데 사용할 수 있는 옵션 중에는 성능이 더 우수한 옵션이 있습니다.
나쁨: 컨테이너 요소의 너비와 높이 애니메이션
CSS를 사용하여 컨테이너 요소의 너비와 높이에 애니메이션을 적용하는 것을 상상할 수 있습니다.
.menu {
overflow: hidden;
width: 350px;
height: 600px;
transition: width 600ms ease-out, height 600ms ease-out;
}
.menu--collapsed {
width: 200px;
height: 60px;
}
이 접근 방식의 즉각적인 문제는 width
및 height
애니메이션이 필요하다는 점입니다.
이러한 속성은 레이아웃을 계산하고 애니메이션의 모든 프레임에 결과를 페인트해야 하므로 비용이 많이 들 수 있으며 일반적으로 60fps를 놓치게 됩니다. 렌더링에 대해 잘 모르겠다면 렌더링 성능 가이드를 참고하세요. 렌더링 프로세스의 작동 방식에 관한 자세한 내용을 확인할 수 있습니다.
나쁨: CSS clip 또는 clip-path 속성 사용
width
및 height
에 애니메이션을 적용하는 대신 (현재 지원 중단됨) clip
속성을 사용하여 펼치기 및 접기 효과에 애니메이션을 적용할 수 있습니다. 또는 원하는 경우 clip-path
를 대신 사용할 수 있습니다. 그러나 clip-path
는 clip
보다 지원이 덜됩니다. 하지만 clip
는 지원 중단되었습니다. 오른쪽 하지만 실망하지 마세요. 원하신 솔루션이 아니긴 하지만요.
.menu {
position: absolute;
clip: rect(0px 112px 175px 0px);
transition: clip 600ms ease-out;
}
.menu--collapsed {
clip: rect(0px 70px 34px 0px);
}
메뉴 요소의 width
및 height
에 애니메이션을 적용하는 것보다 낫지만 이 접근 방식의 단점은 여전히 페인트를 트리거한다는 점입니다. 또한 이 방법을 사용하는 경우 clip
속성은 작업하는 요소가 절대 또는 고정된 위치여야 하므로 약간의 추가 조정이 필요할 수 있습니다.
좋음: 축척 애니메이션
이 효과에는 점점 커지는 것이 포함되므로 배율 변환을 사용할 수 있습니다. 변환 변경에는 레이아웃이나 페인트가 필요하지 않고 브라우저가 GPU에 전달할 수 있으므로 효과가 빨라지고 60fps에 도달할 가능성이 훨씬 높기 때문에 이는 좋은 소식입니다.
렌더링 성능의 대부분과 마찬가지로 이 접근 방식의 단점은 약간의 설정이 필요하다는 점입니다. 그래도 그럴 만한 가치가 있습니다!
1단계: 시작 상태 및 종료 상태 계산
크기 애니메이션을 사용하는 접근 방식의 첫 번째 단계는 메뉴가 접히거나 펼쳐질 때의 크기를 알려주는 요소를 읽는 것입니다. 어떤 경우에는 이러한 정보를 한 번에 모두 얻을 수 없으며 구성요소의 다양한 상태를 읽을 수 있도록 일부 클래스를 전환해야 할 수도 있습니다.
하지만 이렇게 해야 하는 경우에는 주의해야 합니다. 마지막 실행 이후 스타일이 변경된 경우 getBoundingClientRect()
(또는 offsetWidth
및 offsetHeight
)는 브라우저가 스타일 및 레이아웃 패스를 실행하도록 강제합니다.
function calculateCollapsedScale () {
// The menu title can act as the marker for the collapsed state.
const collapsed = menuTitle.getBoundingClientRect();
// Whereas the menu as a whole (title plus items) can act as
// a proxy for the expanded state.
const expanded = menu.getBoundingClientRect();
return {
x: collapsed.width / expanded.width,
y: collapsed.height / expanded.height
};
}
메뉴와 같은 경우 자연스러운 크기(1, 1)로 시작한다고 가정할 수 있습니다. 이 자연스러운 크기는 확장된 상태를 나타냅니다. 즉, 위에서 계산된 축소된 버전에서 자연스러운 크기로 다시 애니메이션을 적용해야 합니다.
하지만 이렇게 하면 메뉴의 내용도 확장됩니다. 아래에서 확인할 수 있듯이, 예.
이 문제를 해결하려면 어떻게 해야 하나요? 콘텐츠에 역변환을 적용할 수 있습니다. 예를 들어 컨테이너가 일반 크기의 1/5로 축소된 경우 콘텐츠가 찌그러지지 않도록 콘텐츠를 5배 확대할 수 있습니다. 이때 알아야 할 두 가지 사항이 있습니다.
또한 카운터 변환은 배율 연산입니다. 이는 컨테이너의 애니메이션과 마찬가지로 가속할 수도 있으므로 좋습니다. 애니메이션이 적용되는 요소가 자체 컴포저이터 레이어를 가져와야 할 수 있습니다(GPU가 지원되도록 설정). 이를 위해 요소에
will-change: transform
를 추가하거나 이전 브라우저를 지원해야 하는 경우backface-visiblity: hidden
를 추가하면 됩니다.역변환은 프레임별로 계산되어야 합니다. 이 경우 좀 더 까다로울 수 있습니다. 애니메이션이 CSS에 있고 이징 함수를 사용한다고 가정하면 카운터 변환을 애니메이션화할 때 이징 자체를 카운터해야 하기 때문입니다. 하지만 예를 들어
cubic-bezier(0, 0, 0.3, 1)
의 역 곡선을 계산하는 것은 그리 간단하지 않습니다.
따라서 JavaScript를 사용하여 효과에 애니메이션을 적용하는 것이 좋습니다. 그러면 완화 공식을 사용하여 프레임당 크기 조정 값과 크기 조정 역값을 계산할 수 있습니다. JavaScript 기반 애니메이션의 단점은 JavaScript가 실행되는 기본 스레드가 다른 작업으로 바쁠 때 발생하는 상황입니다. 간단히 말해 애니메이션이 끊기거나 완전히 중지될 수 있으며 이는 UX에 좋지 않습니다.
2단계: 즉시 CSS 애니메이션 빌드
처음에는 이상하게 보일 수 있는 해결 방법은 자체 이중 값 함수로 키프레임 애니메이션을 동적으로 만들고 메뉴에서 사용할 수 있도록 페이지에 삽입하는 것입니다. (이 문제를 지적해 주신 Chrome 엔지니어 로버트 플랙님께 감사드립니다.) 이 방법의 주요 이점은 변형을 변경하는 키프레임 애니메이션을 컴포저이터에서 실행할 수 있다는 것입니다. 즉, 기본 스레드의 작업의 영향을 받지 않습니다.
키프레임 애니메이션을 만들려면 0에서 100까지 단계를 진행하고 요소와 콘텐츠에 필요한 크기 조정 값을 계산합니다. 그런 다음 이를 문자열로 축약하여 스타일 요소로 페이지에 삽입할 수 있습니다. 스타일을 삽입하면 페이지에 스타일 다시 계산이 전달되며 이는 브라우저에서 해야 하는 추가 작업이지만 구성요소가 부팅될 때 한 번만 실행됩니다.
function createKeyframeAnimation () {
// Figure out the size of the element when collapsed.
let {x, y} = calculateCollapsedScale();
let animation = '';
let inverseAnimation = '';
for (let step = 0; step <= 100; step++) {
// Remap the step value to an eased one.
let easedStep = ease(step / 100);
// Calculate the scale of the element.
const xScale = x + (1 - x) * easedStep;
const yScale = y + (1 - y) * easedStep;
animation += `${step}% {
transform: scale(${xScale}, ${yScale});
}`;
// And now the inverse for the contents.
const invXScale = 1 / xScale;
const invYScale = 1 / yScale;
inverseAnimation += `${step}% {
transform: scale(${invXScale}, ${invYScale});
}`;
}
return `
@keyframes menuAnimation {
${animation}
}
@keyframes menuContentsAnimation {
${inverseAnimation}
}`;
}
호기심이 많은 사람은 for 루프 내의 ease()
함수에 관해 궁금해할 수 있습니다. 다음과 같은 방법을 사용하여 0~1의 값을 완화된 등가 값에 매핑할 수 있습니다.
function ease (v, pow=4) {
return 1 - Math.pow(1 - v, pow);
}
Google 검색을 사용하여 그래프를 표시할 수도 있습니다. 편리합니다. 다른 원활화 방정식이 필요한 경우 다양한 원활화 방정식이 포함된 솔레다드 페나데스님의 Tween.js를 확인하세요.
3단계: CSS 애니메이션 사용 설정
이러한 애니메이션을 만들고 JavaScript에서 페이지로 베이킹했으므로 마지막 단계는 애니메이션을 사용 설정하는 클래스를 전환하는 것입니다.
.menu--expanded {
animation-name: menuAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
.menu__contents--expanded {
animation-name: menuContentsAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
이렇게 하면 이전 단계에서 만든 애니메이션이 실행됩니다. 베이킹된 애니메이션은 이미 eased가 되어 있으므로 타이밍 함수를 linear
로 설정해야 합니다. 그렇지 않으면 각 키프레임 사이를 이동하게 되어 매우 이상하게 표시됩니다.
요소를 다시 접는 방법에는 두 가지가 있습니다. CSS 애니메이션을 업데이트하여 앞으로가 아닌 뒤로 실행되도록 합니다. 이렇게 하면 괜찮게 작동하지만 애니메이션의 '느낌'이 반전됩니다. 따라서 ease-out 곡선을 사용했다면 역방향이 진입이 완화된 것처럼 느껴져 느리게 느껴질 수 있습니다. 더 적절한 해결 방법은 요소를 접을 때 사용할 두 번째 애니메이션 쌍을 만드는 것입니다. 이는 확장 키프레임 애니메이션과 정확히 동일한 방식으로 만들 수 있지만 시작 값과 끝 값을 전환합니다.
const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;
고급 버전: 원형 공개
이 기법을 사용하여 원형 펼치기 및 접기 애니메이션을 만들 수도 있습니다.
원리는 요소의 크기를 조절하고 즉각적인 하위 요소의 크기를 조절하는 이전 버전과 거의 동일합니다. 이 경우 크기를 조절하는 요소의 border-radius
는 50%이므로 원형이 되며 overflow: hidden
가 있는 다른 요소로 래핑됩니다. 즉, 원이 요소 경계 밖으로 확장되지 않습니다.
이 특정 변형에 관한 주의: 애니메이션 중에 Chrome이 낮은 DPI 화면에서 흐릿한 텍스트가 발생합니다. 세부정보에 관심이 있는 경우 별표표시하고 추적할 수 있는 버그가 신고되어 있습니다.
원형 확장 효과의 코드는 GitHub 저장소에서 확인할 수 있습니다.
결론
이제 크기 변환을 사용하여 성능이 우수한 클립 애니메이션을 실행하는 방법을 알게 되었습니다. 이상적인 상황에서는 클립 애니메이션이 가속화되는 것이 좋습니다 (Jake Archibald이 작성한 이 문제와 관련된 Chromium 버그가 있음). 하지만 그때까지는 clip
또는 clip-path
애니메이션을 만들 때 주의해야 하며 width
또는 height
애니메이션은 절대 피해야 합니다.
이와 같은 효과에는 웹 애니메이션을 사용하는 것도 편리합니다. 웹 애니메이션에는 JavaScript API가 있지만 transform
및 opacity
에만 애니메이션을 적용하는 경우 컴포지터 스레드에서 실행할 수 있기 때문입니다.
안타깝게도 웹 애니메이션 지원이 좋지 않습니다. 하지만 사용 가능한 경우 프로그레시브 개선을 사용하여 사용할 수 있습니다.
if ('animate' in HTMLElement.prototype) {
// Animate with Web Animations.
} else {
// Fall back to generated CSS Animations or JS.
}
상황이 바뀔 때까지는 JavaScript 기반 라이브러리를 사용하여 애니메이션을 실행할 수 있지만 있지만 CSS 애니메이션을 베이킹하여 대신 사용하면 더 안정적인 성능을 얻을 수 있습니다. 마찬가지로, 앱이 이미 애니메이션에 JavaScript를 사용하고 있다면 최소한 기존 코드베이스와의 일관성을 유지하는 것이 좋습니다.
이 효과의 코드를 살펴보려면 UI 요소 샘플 GitHub 저장소를 살펴보고 언제나 그렇듯이 아래 댓글을 통해 진행 상황을 알려주세요.