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로 살짝씩 끌어당기는 구조다.
실제 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으로 올리면 거의 즉시 붙는다.