드래그 앤 세틀

Drag and settle

드래그 중에는 커서를 살짝 고무줄처럼 지연되어 따라가고, 손을 떼면 관성으로 더 나아갔다가 스프링으로 제자리에 안착하는 인터랙션. 팔로우 스루·오버래핑 액션을 실시간 포인터 입력으로 확장한 버전입니다.

다른 이름: Drag inertiaRelease and settle
···
html
<div class="stage">
  <div class="track"></div>
  <i class="ball" id="dball"></i>
  <i class="cursor" id="dcursor"></i>
</div>
css
.stage{position:absolute;inset:0}
.track{position:absolute;left:8%;right:8%;top:50%;height:2px;margin-top:-1px;background:var(--line)}
.ball{position:absolute;top:50%;--d:clamp(20px,9vmin,34px);width:var(--d);height:var(--d);
  margin-top:calc(var(--d) / -2);margin-left:calc(var(--d) / -2);
  border-radius:50%;background:var(--accent);box-shadow:0 4px 10px rgba(0,0,0,.2)}
.cursor{position:absolute;top:50%;--d:clamp(12px,5vmin,20px);width:var(--d);height:var(--d);
  margin-top:calc(var(--d) / -2);margin-left:calc(var(--d) / -2);
  border-radius:50%;background:var(--fg);opacity:0}
js
const ball = document.getElementById('dball');
const cursor = document.getElementById('dcursor');
const START = 0.12, TARGET = 0.72;
const GRAB_AT = 500, DRAG_END = 1900, CYCLE = 3400;
let ballX = START, vx = 0;
const startT = performance.now();
function frame(now) {
  const t = (now - startT) % CYCLE;
  if (t < 20) { ballX = START; vx = 0; }
  let goal, cursorX, cursorOpacity;
  if (t < GRAB_AT) {
    const p = t / GRAB_AT;
    cursorX = -0.08 + (START - -0.08) * p;
    cursorOpacity = Math.min(p * 3, 1);
    goal = START;
  } else if (t < DRAG_END) {
    const p = (t - GRAB_AT) / (DRAG_END - GRAB_AT);
    cursorX = START + (TARGET - START) * p;
    cursorOpacity = 1;
    goal = cursorX;
  } else {
    cursorX = TARGET;
    cursorOpacity = Math.max(1 - (t - DRAG_END) / 260, 0);
    goal = TARGET;
  }
  cursor.style.left = (cursorX * 100) + '%';
  cursor.style.opacity = String(cursorOpacity);

  const k = 0.05, damp = 0.8;
  vx = (vx + (goal - ballX) * k) * damp;
  ballX += vx;
  ball.style.left = (ballX * 100) + '%';
  const stretch = Math.min(Math.abs(vx) * 22, 0.45);
  ball.style.transform = 'scaleX(' + (1 + stretch) + ') scaleY(' + (1 - stretch * 0.6) + ')';

  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

전통 애니메이션의 팔로우 스루·오버래핑 액션은 미리 그려진 시퀀스였지만, 드래그는 사용자가 실시간으로 입력하기 때문에 정해진 인비트윈이 없습니다. 그래서 요즘 UI는 물리 시뮬레이션(스프링)으로 이 감각을 실시간으로 만들어냅니다 — 매 프레임 "목표 지점(커서 또는 최종 위치)"과 "현재 위치"의 차이만큼 힘을 계산해서 위치를 갱신합니다.

드래그하는 동안에는 목표가 커서 자체라서, 물체가 커서에 딱 붙지 않고 살짝 뒤처지며 고무줄처럼 따라옵니다. 손을 떼는 순간 목표가 최종 정착 지점으로 바뀌지만 그때까지 쌓인 속도(관성)는 그대로 남아있어서, 물체가 목표를 살짝 지나쳤다가(오버슈트) 감쇠하며 제자리에 안착합니다.

iOS의 시트, Trello의 카드 드래그, macOS 창 스냅이 전부 이 감각을 씁니다 — 값이 딱 맞아떨어지는 게 아니라, 붙잡혀서 움직이다가 놓아지는 물리적인 느낌이 핵심입니다.

데모에서 가짜 커서가 공을 잡고 오른쪽으로 끌고 가면, 공은 커서보다 살짝 늦게 따라오다가(드래그 중 래그) 커서가 놓이는 순간 관성으로 조금 더 나아갔다 튕기며 멈춥니다(릴리스 후 오버슈트·세틀).

언제 쓰나

실제 드래그·스와이프·바텀시트처럼 사용자가 손을 놓는 순간이 있는 인터랙션에 씁니다. 클릭 한 번으로 끝나는 동작에는 스프링 세틀까지 갈 필요 없습니다 — 보통의 이징이면 충분합니다.