아크

Arcs

살아있는 대부분의 움직임은 완전한 직선이 아니라 살짝 휜 곡선을 그린다는 원칙. 팔은 어깨를, 던진 공은 중력을 축으로 돌기 때문입니다.

다른 이름: Arc motionMotion arcs
···
html
<div class="lane-label l1">Linear</div>
<div class="lane-label l2">Arc</div>
<canvas id="ac"></canvas>
css
.lane-label{position:absolute;left:6%;font:700 clamp(10px,2.8vmin,13px)/1 ui-monospace,monospace;color:var(--muted);z-index:2}
.l1{top:10%}
.l2{top:50%}
#ac{position:absolute;inset:0;width:100%;height:100%;display:block}
js
const canvas = document.getElementById('ac');
const ctx = canvas.getContext('2d');
const cs = getComputedStyle(document.documentElement);
function col(name, fb) { const v = cs.getPropertyValue(name).trim(); return v || fb; }
const LINE = col('--line', '#e2e0d8');
const ACCENT = col('--accent', '#5b5bf7');
const ACCENT2 = col('--accent-2', '#f25c8a');
let w = 0, h = 0;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  canvas.width = w * dpr; canvas.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();

function ball(x, y, r, color, alpha) {
  ctx.globalAlpha = alpha === undefined ? 1 : alpha;
  ctx.beginPath(); ctx.arc(x, y, Math.max(r, 1), 0, Math.PI * 2);
  ctx.fillStyle = color; ctx.fill(); ctx.globalAlpha = 1;
}

const DUR = 2400;
const startT = performance.now();
function loop(now) {
  const t = ((now - startT) % DUR) / DUR;
  ctx.clearRect(0, 0, w, h);
  const x0 = w * 0.12, x1 = w * 0.88;
  const y1 = h * 0.32, y2 = h * 0.78, apex = h * 0.36;
  const r = h * 0.055;

  for (let k = 6; k >= 1; k--) {
    const tt = ((t - k * 0.03) + 1) % 1;
    ball(x0 + (x1 - x0) * tt, y1, r * 0.75, ACCENT, 0.07 * (7 - k));
  }
  ball(x0 + (x1 - x0) * t, y1, r, ACCENT);

  ctx.strokeStyle = LINE; ctx.lineWidth = 1;
  ctx.setLineDash([2, 4]);
  ctx.beginPath(); ctx.moveTo(x0, y2); ctx.lineTo(x1, y2); ctx.stroke();
  ctx.setLineDash([]);
  ctx.beginPath();
  for (let i = 0; i <= 30; i++) {
    const tt = i / 30;
    const px = x0 + (x1 - x0) * tt;
    const py = y2 - Math.sin(tt * Math.PI) * (y2 - apex);
    if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
  }
  ctx.stroke();
  for (let k = 6; k >= 1; k--) {
    const tt = ((t - k * 0.03) + 1) % 1;
    const px = x0 + (x1 - x0) * tt;
    const py = y2 - Math.sin(tt * Math.PI) * (y2 - apex);
    ball(px, py, r * 0.75, ACCENT2, 0.07 * (7 - k));
  }
  const px = x0 + (x1 - x0) * t;
  const py = y2 - Math.sin(t * Math.PI) * (y2 - apex);
  ball(px, py, r, ACCENT2);

  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

팔을 휘두르면 어깨 관절을 축으로 도는 호(弧)를 그리고, 던진 공은 중력 때문에 포물선을 그립니다. 애니메이터는 캐릭터가 움직이는 경로를 먼저 아크로 그려두고, 그 곡선 위에 인비트윈들을 배치합니다. 어느 한 그림이 이 곡선에서 벗어나면 그 프레임만 튀어 보여서 위화감이 생깁니다.

완전히 직선으로 움직이는 것은 오히려 부자연스럽습니다 — 기계 팔이나 레이저처럼 실제로 기계적인 대상에게만 어울립니다. 그래서 캐릭터·유기적 형태의 이동은 대부분 아크를 씁니다.

UI에서는 모달이 뜨는 경로, 커서를 따라가는 툴팁, 드래그 후 튕겨 돌아오는 카드에 살짝 곡선 경로(motion-path 또는 두 단계 transform)를 주면 훨씬 자연스러워집니다.

데모 위쪽은 직선 경로 — 목적지까지 최단 거리로만 갑니다. 아래쪽은 같은 두 지점을 아크로 잇습니다. 둘 다 지나온 자리에 옅은 잔상을 남겨 경로의 모양이 그대로 드러납니다.

언제 쓰나

캐릭터·아이콘·드래그 같은 유기적인 이동에는 아크를 쓰세요. 값이 정확히 어디로 가는지가 중요한 데이터 시각화에는 직선이 더 명확합니다.