내가 가장 좋아하는 애니메이션 기법: 지수 평활화 (2023)
(lisyarus.github.io)- 지수 평활화는 토글 버튼, 카메라, UI 요소, 오디오 볼륨처럼 목표값을 향해 부드럽게 따라가야 하는 애니메이션에 넓게 쓸 수 있는 단순한 기법임
- 핵심 식은
position += (target - position) * (1 - exp(- speed * dt))이며, 현재 위치와 목표 위치만으로 목표 변경에 자연스럽게 반응함 - 선형 이동과 일반적인 이징(easing)은 중간 클릭, 빠른 카메라 입력, 큰
dt에서 점프·떨림·입력 큐 같은 처리가 필요하지만, 지수 평활화는 같은 구조로 복잡도를 줄임 1 - exp(- speed * dt)는 비례 갱신식이 푸는 미분방정식의 해에서 나오며, 작은dt에서는 기존 식과 거의 같고 큰dt에서도 오버슈트를 피함- 목표값에 수학적으로 정확히 도달하지는 않지만, 부동소수점 정밀도와 사용자가 감지할 수 있는 변화량 한계 때문에 실제 애니메이션에서는 충분히 완료된 것처럼 동작함
토글 버튼에서 드러나는 문제
- 토글 버튼의 스위치 위치는
turned_on ? max_x : min_x처럼 단순히 계산할 수 있지만, 상태가 바뀔 때 위치가 즉시 순간이동해 생동감이 부족함 - 선형 애니메이션은 현재 위치를 일정 속도로 갱신하고 범위를 제한하는 방식으로 구현됨
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
- 일정 속도 이동은 위치가 시간에 대해 선형 함수가 되어 움직임이 둔하게 보일 수 있음
- 이징 함수를 얹으면 움직임을 더 부드럽게 만들 수 있음
- 고전적인 cubic smoothstep:
3t² - 2t³ - 제곱근 이징:
sqrt(t)
- 고전적인 cubic smoothstep:
smoothstep은1 - f(t) = f(1 - t)대칭성이 있어 앞뒤 애니메이션에 같은 코드를 쓸 수 있음sqrt는 방향에 따라 다른 식이 필요함- 켜질 때:
sqrt(t) - 꺼질 때:
1 - sqrt(1 - t)
- 켜질 때:
sqrt는 시작 시 빠르게 움직이고 목표에 가까워질수록 부드럽게 느려지지만, 단순한 2상태 토글에서도t, 방향, 이징 계산 같은 상태 관리가 필요함- 사용자가 애니메이션 중간에 다시 클릭하면 기존 이징 방식은 위치가 갑자기 점프하는 불연속성을 만들 수 있음
지수 평활화 식
- 지수 평활화는 목표 위치를 정한 뒤 현재 위치를 목표 쪽으로 조금씩 당기는 방식임
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
- 갱신량은 현재 위치와 목표 위치의 차이인
target - position.x를 기준으로 정해짐 1 - exp(- dt * speed)는 프레임마다 목표 쪽으로 얼마나 이동할지 결정하는 보간 계수임- 중간에 목표가 바뀌어도 별도의 진행 시간이나 애니메이션 방향 상태 없이 새 목표를 향해 바로 이어서 움직임
- 토글 버튼 예시에서는
sqrt이징처럼 빠르게 시작하고 목표 근처에서 느려지면서도, 중간 클릭 시 점프 문제가 줄어듦
카메라 이동에서 더 커지는 장점
- 맵 위를 이동하는 카메라에도 같은 문제가 나타남
- 일정 속도 보간을 단순하게 구현하면 축별로 목표 방향의 부호를 사용함
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
- 애니메이션 완료 지점에서
target - position의 부호가 양수와 음수 사이를 오가며 떨림이 생길 수 있음 - 이를 막으려면
delta를max_delta범위로 제한하는 별도 갱신 함수가 필요함
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
- cubic 이징을 카메라 이동에 적용하려면 요청된 이동 이벤트를 큐에 넣고 하나씩 처리해야 해 구조가 복잡해짐
- 애니메이션 중 사용자 입력을 무시하는 방식은 사용자 입장에서 매우 답답하게 느껴짐
- 지수 평활화를 쓰면 토글 버튼과 거의 같은 코드로 카메라 이동을 처리할 수 있음
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
- 사용자가 빠르게 클릭하면 목표와 현재 위치의 차이가 커지므로 카메라가 자연스럽게 더 빨라짐
왜 1 - exp(- speed * dt)인가
- 단순한 비례 갱신은 다음 식으로 쓸 수 있음
position += (target - position) * speed * dt;
- 이 식은 목표와 현재 위치의 차이가 클수록 빠르게 움직이고, 현재 위치와 목표 위치 외에는 추가 상태가 필요 없음
lerp로 쓰면 같은 식이 됨
position = lerp(position, target, speed * dt);
speed * dt가 0에 가까우면 느리게 움직이고, 1에 가까우면 빠르게 목표에 접근함speed * dt가 1보다 크면 보간이 목표를 지나치는 오버슈트가 발생함- 예시에서
speed = 220,dt = 1 / 125일 때speed * dt가 1보다 커져 떨림이 나타남 speed * dt < 2이면 절대적인 차이는 줄어들 수 있지만,speed * dt > 2이면 유용한 동작을 하지 못함
- 예시에서
- 보간 계수를
min(1, speed * dt)로 제한할 수는 있지만,dt가 커지는 상황까지 매끄럽게 처리하는 해법은 아님- 코드가 느려 프레임률이 떨어질 수 있음
- 사용자가 다른 탭이나 창으로 이동해 코드가 멈췄다가 여러 초의
dt로 깨어날 수 있음
- 물리 시뮬레이션에서는 큰
dt를 제한하거나 여러 업데이트로 나눌 수 있지만, 애니메이션에서는 카메라와 버튼이 큰dt에서도 자연스럽게 동작하는 편이 사용자 경험에 좋음
미분방정식으로 본 해법
A += B * dt형태는 일반적으로dA/dt = B라는 미분방정식의 수치적 풀이에 해당함- 단순 갱신식은 다음 방정식을 푸는 형태임
d(position) / dt = (target - position) * speed
- 변수를
x = position,a = target,c = speed로 두면 다음과 같음
dx / dt = (a - x) * c
- 이 방정식을 직접 풀면 다음 형태가 나옴
x = x0 + (a - x0) * (1 - exp(-c * t))
- 따라서 작은
dt에서 맞는 식이position += (target - position) * speed * dt라면, 임의의dt에서 쓸 수 있는 식은 다음과 같음
position += (target - position) * (1 - exp(- speed * dt));
- 테일러 전개에서
exp(x) ≈ 1 + x이므로, 작은dt에서는1 - exp(-speed * dt) ≈ speed * dt가 되어 기존 단순식과 같아짐 speed * dt가 매우 커도exp(-speed * dt)는 0에 가까워지고1 - exp(...)는 1에 가까워져, 목표에 가까운 값으로 안정적으로 이동함- 같은 식은
lerp로도 쓸 수 있음
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));
속도 값 고르기
- 일반적인 애니메이션은 “0.125초 안에 이동”처럼 지속 시간으로 생각하기 쉽지만, 지수 평활화는 기술적으로 무한한 시간이 지나야 목표에 정확히 도달함
exp(- speed * time)은 시간이 지날수록 작아지지만 0이 되지는 않으므로, 시작값과 목표값이 다르면position도 수학적으로는target과 완전히 같아지지 않음- 실제로는 부동소수점 정밀도 한계에 도달하거나, 카메라 위치 차이가 사용자가 볼 수 없을 만큼 작아져 애니메이션이 끝난 것처럼 보임
speed의 의미는1 / speed시간 동안position이target에 정확히e = 2.71828...배 더 가까워진다는 것임- 실사용에서는 보통
speed를5..50범위로 두며, 같은 느낌의 선형·cubic 애니메이션 속도에 대해서는 지수 평활화의2 * speed정도가 적절하게 느껴질 수 있음
신호 처리와의 연결
- “exponential smoothing” 또는 “exponential moving average”로 검색하면 Wikipedia의 Exponential smoothing 문서를 찾을 수 있음
- 고정된
dt에서target이 매 반복마다 바뀌는 경우, 값은 반복 인덱스 기준으로 다음과 비슷하게 갱신됨factor = 1 - exp(- speed * dt)- 보통은
factor를 0과 1 사이 값으로 직접 정함
- 이산적인 지수 평활화는 애니메이션에서 쓰는 방식의 이산 아날로그임
- 신호 처리에서도 이전 값 목록이나 복잡한 상태를 유지하지 않고 현재 평균값만으로 동작한다는 이유로 사용됨
- 디지털 오디오에서는 보통
dt가 샘플링 주파수의 역수인1 / freq로 고정됨- 예:
1/44100,1/48000
- 예:
댓글과 토론
Hacker News 의견들
-
여기서 핵심이 충분히 다뤄지지 않는 듯함. 이건 단순히 0과 1 사이의 또 다른 이징 곡선이나
smoothstep()이 아니라, 거의 어떤 입력이든 규칙적으로 처리하는 무상태 방식이라 정말 유용함
CSS 전환을 써봤다면 이 문제가 익숙할 것임. 지속 시간을 400ms로 잡았다면 왜 하필 400ms인가? 이동해야 할 거리에 따라 달라져야 하지 않나?
다른 이들이 말했듯 지수 평활에는 목적지에 점근적으로 가까워질 뿐 결코 도달하지 않는 문제가 있음. 단계가 임계값보다 작아지면 애니메이션을 멈추는 뻔한 해결책은 있지만 우아하지 않음
관성 스크롤에 비슷한 방식을 쓸 때는 유사 마찰 항을 추가하는 게 유용했음. 이 항이 지수 항을 상쇄해서 사실상 최소 속도처럼 동작함. Desmos 예시는 여기: https://www.desmos.com/calculator/98ufbuzxhj- 지수 평활을 목적지까지의 거리
D에 대한 상미분방정식으로 해석하면 해결할 수 있을지도 모름. 지수 평활은dD/dt=-C*D의 오일러 갱신이고, 해가D(t)=A*exp(-C*t)라서 0에 점근하지만 도달하지 않음
이를 유한 시간 안에 0으로 가는 식, 예를 들어dD/dt=-C*sqrt(D)로 바꾸면 됨. 해는 반쪽짜리 2차식처럼 움직이다가 0에 도달하면 그대로 머무름. 이 식의 오일러 갱신도 원하는 대로 무상태임 - 단계가 임계값 아래로 내려가면 애니메이션을 멈추는 게 왜 우아하지 않은지 모르겠음. 단순하고 구현과 검증이 쉬우며, 원하는 시각적 효과도 제공하니 충분히 깔끔해 보임
- CSS 전환의 400ms 문제가 있다면, 지수 함수의 속도 매개변수도 같은 문제를 갖는 것 아닌가 싶음
- 지수 평활을 목적지까지의 거리
-
게임 개발자로서는 대부분의 UI에는 미리 정한 지속 시간을 가진 이징 트윈이 더 낫다고 봄. 하지만 시작점과 끝점이 명확하지 않은 연속적·예측 불가능한 움직임을 부드럽게 만들고 싶을 때는 이 다른 종류의 애니메이션이 매우 유용함
예를 들어 플레이어가 마우스로 격자 위 타일을 드래그할 때 격자에 스냅되게 하거나, 글의 예시처럼 카메라를 움직이는 경우가 그렇다
이런 경우 지수 보간 요령은 매우 유용하지만 널리 알려져 있지는 않음. 많은 게임이 더 부정확한 선형 보간을 쓰다가, 60fps가 표준이던 시절과 달리 누군가 240Hz 모니터에서 실행하면 애니메이션 느낌이 완전히 이상해지는 문제를 겪음
그래서 이 글이 반가움. 이런 지나치게 구체적인 지식은 대개 팀의 선임이 후임에게 도제식 구전으로 전하는 식이라 접근하기 어렵다 -
글은 마음에 들지만, 토글 스위치에서는
sqrt가 3차 함수보다 낫다는 저자의 판단은 객관적으로 틀렸다고 말하고 싶음. 실제 토글 스위치가 대체로 작동하는 방식을 보면 이 상황에는 3차 함수가 더 나은 선택임
집의 전기 차단기 스위치나 아날로그 신스, 오디오 장비에 자주 보이는 스위치를 떠올리면 됨. 특정한 미감을 노리는 장비들인데, 내가 가진 작은 Hughes & Kettner 기타 앰프에도 누르는 맛이 좋은 스위치 두 개가 있음
이런 스위치는 처음에 약간 저항이 있다가, 스프링 구조 때문에 갑자기 새 위치로 탁 넘어감. 이 동작은sqrt나 지수 평활보다 3차 함수가 더 잘 모델링함
그 사소한 지적을 빼면 글은 아주 좋았음. 적절한 이징 함수처럼 애니메이션을 잘 쓰면 사용자 경험을 향상시키지만, 선형 보간 예시처럼 신중하지 않게 구현하면 거슬리고 경험을 해칠 수 있다는 점을 잘 보여줌
[0] 토글 스위치 종류에 따라 다르긴 함. 다만 Minimoog 같은 장비에서 볼 수 있는 종류도 “저항하다가 새 위치로 탁 넘어가는” 동작을 보여서 조작이 즐거움. 참고로 장비 자랑은 아니고, 나는 Minimoog를 갖고 있지 않음- 직관적으로도 3차 함수 쪽 느낌이 더 좋았음. 기계식 토글에는 어느 정도 관성이 있고, 그게 3차 접근에서 보이는 것 같음
-
단순한 비선형 요령이 온라인 상호작용에 즐거움을 더하는 일이 얼마나 잦은지 계속 놀라게 됨. 색 지각의 경우에는 두 색이 어떤 사람에게 충분히 구분되지 않는 이유를 이해하는 데 핵심적이기도 함
이상한 점은 인간이 항상 가속을 잘 이해하지는 못한다는 것임. 불이 평지에서처럼 거의 일정한 속도로 간다고 믿고 언덕 위로 도망가면 안 됨. 불은 언덕을 오르며 가속함
아이들은 던진 공이 지면을 따라 움직이는 속도는 빠르게 배우지만, 중력 때문에 손에 부딪힐 때 얼마나 빠르게 움직일지는 항상 잘 이해하지 못함 -
이 글의 대부분이 결국 이징으로 귀결된다는 점이 흥미로움. 새로운 세대마다 이것을 스스로 다시 발견해야 하는 것처럼 보임
90년대 후반 Yugo Nakamura의 실험적 웹사이트에 매료됐던 기억이 있음. 이징을 자유롭게 사용해 유기적인 느낌을 준, 내가 본 초기 웹사이트 중 하나였음: https://www.youtube.com/watch?v=NLt7Gwnt3WY- Yugo가 발표하던 컨퍼런스 맨 앞줄에 앉아 있었고, 데모를 훑어 보여주자 다른 Flash 전문가들이 완전히 열광하던 장면이 기억남
-
왠지 이런 토글이 있으면 좋겠음. 터치하거나 클릭한 상태에서는 목표의 약 75%까지 천천히 이동하고, 놓으면 나머지를 탁 이동하는 방식
UX 관점에서 무슨 의미가 될지는 잘 모르겠음. 설정이 마지막에 실제 적용되거나 저장된다는 뜻일 수도 있음
또는 “정말 확실한가요?” 대화상자의 일부일 수도 있음. 누르고 있는 동안 설정이 적용되지만, 제자리에 탁 들어가기 전에는 Escape로 되돌릴 수 있는 식- https://jsfiddle.net/u1vybhqg/
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }
input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; } - 많은 고령 사용자가 토글을 밀어 보려다가 아무 일도 일어나지 않는 모습을 봤음. 그러니 그렇게 만들 거면 토글을 실제로 밀 수 있게도 해야 함. 아니면 그냥 체크박스를 쓰는 편이 낫다
- https://jsfiddle.net/u1vybhqg/
-
이 글이 마음에 듦. 거의 정확히 같은 기법을 10년쯤 전에 작성했었고, 당시에는
lazy-easy라고 불렀으며 지금도 쓰고 있음
때로는 상태 관리를 모두 하지 않고도 부드러운 애니메이션만 원할 때가 있음: https://www.hailpixel.com/articles/lazy-animation-with-lazy-... -
글 자체는 정말 좋음. 데모는 Chrome에서는 잘 동작하는 듯하지만, Firefox에서는 스크롤하는 동안 멈추고 페이지 렌더링이 완전히 중단됨
-
실제로 꽤 좋은 접근이고, 애니메이션·이징 기법의 개념 증명으로도 좋음. Flickity가 많이 떠오름
https://metafizzy.co/blog/initial-demos/
https://metafizzy.co/blog/math-time-resting-position/
https://metafizzy.co/blog/particle-to-slider/
https://metafizzy.co/blog/flickity-begins/
특히 이 데모: https://codepen.io/desandro/pen/myXdej
이 기법은 스위치에만 유용한 게 아님. 실제로 스위치에 이런 요소를 쓰지도 않을 것이고, 사이트 전체에서requestAnimationFrame루프 20개를 병렬로 돌리지도 않을 것임. 일부러 망가진 요소를 넣지도 않을 것임
또한 델타가 충분히 작아지면 렌더링을 멈추는 최적화도 없고, 프로덕션 수준으로 만들기 위한 작은 조정이 수십 가지 더 있을 가능성이 큼
여기 반응을 보면 글을 읽지 않고 가정하거나, 나무만 보고 숲을 못 보거나, 너무 편향적이고 냉소적이라 똑똑해 보이려고 비전문적인 판단을 공유하는 것처럼 보임
언제부터 HN이 Reddit처럼 된 건가?- 이 경우에는 많은 사람이 글을 읽지 못함. 페이지를 3분의 1쯤 내리면 대부분의 비 Chromium 브라우저가 멈춰 버리기 때문임
- “언제부터 HN이 Reddit처럼 된 건가?”라고 묻는다면 https://news.ycombinator.com/newsguidelines.html의 마지막 문단만이라도 읽어보면 됨
- 그런 변화는 한동안 진행돼 왔지만, HN 가이드라인상 그런 이야기를 직접 하는 것이 금지되어 있어서 사람들이 말하지 않는 것임. 참 이상한 일임
- 이 경우에는 냉소가 그다지 공정해 보이지 않음
HN이 Reddit처럼 변했다는 부분에 대해서는, 기술 전반에 막연한 냉소적 분위기가 있는지도 모름. 정부와 기업이 등장 중인 강력한 기술을 어떻게 쓰려는지, 개인의 자유를 키우기보다 개인을 제한하고 관리하려는 의도로 구축하는 것 아닌지에 대한 불신이 깔려 있음
-
감성 디자인(https://en.wikipedia.org/wiki/Emotional_Design)의 정수에 가깝다. 아주 작은 애니메이션 뒤에도 말할 것이 많음