Lerp로 요소 움직임 부드럽게 만들기
마우스를 따라다니는 원을 만들 때, mousemove 좌표를 그대로 transform에 박으면 커서에 딱 붙어서 움직인다. 끌려오는 듯한 부드러움이 없었다. 그때 Lerp(Linear Interpolation, 선형 보간)를 처음 써봤다.
선형 보간, 두 점 사이를 비율로 나눈다
선형 보간은 두 점 사이의 값을 직선 거리에 따라 추정하는 방법이다. 시작점 좌표와 끝점 좌표를 알고 있을 때, 그 둘을 잇는 직선 위에서 어느 지점의 값을 비율로 나눠 구한다.
코드로 옮기면 함수 하나로 끝난다.
// s: 시작 지점
// e: 끝 지점
// a: 보간 비율 (0~1 사이)
// 계산식: s + (e - s) * a
function lerp(s, e, a) {
return s + (e - s) * a
}a가 0이면 시작점, 1이면 끝점이 그대로 나온다. 그 사이 값(0.1, 0.5 등)을 넣으면 시작과 끝 사이의 어느 지점이 반환된다.
공식이 부드러워 보이는 이유 — 매 프레임 남은 거리의 일부만 이동
이 함수를 requestAnimationFrame의 콜백 안에서 반복 호출하면, 매 프레임마다 현재 위치(s)와 목표 지점(e) 사이의 거리 중 a 비율만큼만 이동한다. 비율을 0.1로 잡으면 매 프레임 남은 거리의 10%만 따라간다.
처음엔 거리가 멀어서 한 프레임에 크게 움직이고, 목표에 가까워질수록 남은 거리가 작아져 이동량도 작아진다. 끝에 가까울수록 느려지는 ease-out 같은 곡선이 공식에서 그냥 따라나온다. 별도 가속도 계산이 필요 없었다.
requestAnimationFrame은 브라우저가 다음 리페인트 직전에 콜백을 실행해준다(React의 setInterval과 달리 화면 갱신 타이밍과 맞춰져서 끊김이 적다). 그래서 매 프레임마다 한 칸씩 보간한 좌표를 transform에 적용하면 부드럽게 따라오는 효과가 난다.
마우스를 따라다니는 원으로 확인한 동작
전체 흐름을 한 번 그려보면 mousemove로 목표 좌표(mouseX, mouseY)를 계속 갱신해두고, requestAnimationFrame 루프 안에서 현재 좌표(startX, startY)를 목표 좌표 쪽으로 lerp로 살짝씩 끌어당기는 구조다.
flowchart LR
Mouse[mousemove 이벤트] --> Target["목표 좌표<br/>mouseX, mouseY 갱신"]
Target --> Loop["requestAnimationFrame 루프"]
Loop --> Lerp["lerp(start, target, 0.1)"]
Lerp --> Apply["transform 적용"]
Apply --> Loop
실제 HTML 코드는 이렇게 짰다. 빨간 원이 화면 어딘가에서 시작해, 커서를 천천히 쫓아오게 된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
#circle {
position: absolute;
width: 50px;
height: 50px;
top: -25px;
left: -25px;
border-radius: 50%;
background-color: red;
}
</style>
<title>Document</title>
</head>
<body>
<div id="circle"></div>
<script>
const circle = document.getElementById("circle");
let mouseX = 0,
mouseY = 0,
startX = 0,
startY = 0;
window.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
circle.style.transform = `translate(${mouseX}px, ${mouseY}px)`;
});
function frame() {
requestAnimationFrame(frame);
startX = lerp(startX, mouseX, 0.1);
startY = lerp(startY, mouseY, 0.1);
circle.style.transform = `translate(${startX}px, ${startY}px)`;
}
requestAnimationFrame(frame);
function lerp(s, e, a) {
return s + (e - s) * a;
}
</script>
</body>
</html>mousemove 안에서도 한 번 transform을 갱신하지만, 그건 즉각적인 위치 표시고, 실제로 부드럽게 따라오는 움직임은 frame() 안의 lerp 보간이 만든다. 보간 비율 0.1을 0.05로 낮추면 더 느리게 끌려오고, 0.3으로 올리면 거의 즉시 붙는다.