Coderifleman's blog

frontend development stories.

프런트엔드 엔지니어를 위한 베지에 곡선(Bézier Curves) - 3편

프런트엔드 엔지니어를 위한 베지에 곡선(Bézier Curves) - 2편」을 포스팅한 후 시간이 꽤 지났다. 어디까지 이야기했더라? 기억도 가물가물하다. 최근에 강의를 시작하면서 여유가 없었다는 핑계를 대보지만, 뭐가 됐든 게을러서 그렇다. 3편을 기다리신 분이 있었다면 죄송할 따름이다.

글과 관련 없는 얘기는 이쯤 하자. 2편에서는 에버리징과 블렌딩 그리고 인터폴레이션(Interpolation)이라고 부르는 보간을 소개했다. 베지에 곡선을 이해하기 위한 기초 지식이었으며 이 개념만 이해하고 있으면 나머지는 쉽게 이해할 수 있다.

1차 베지에 곡선

우리는 이미 1차 베지에 곡선(Linear Bezier Cuvers)을 경험했다. 2편의 복합 데이터 블렌딩보간 절에서 보여준 예제가 바로 1차 베지에 곡선이다. 차이점이 있다면 단순히 평면상에서 곡선을 그리는 게 아니라 직교좌표계 상에서 그린다는 것이다.

1차 베지에 곡선
<그림 1. 1차 베지에 곡선>

1차 베지에 곡선은 조절점(Control point) 두 개로 그린다. 아주 간단하지만, 굴곡이 없는 선형이다(직선도 곡선에 포함된다는 사실을 잊지 말자).

2차 베지에 곡선

그럼 2차 베지에 곡선(Quadratic Bézier Curves)을 그려보자. 2차 베지에 곡선은 조절점 3개를 이용해 그린 곡선을 말한다.

2차 베지에 곡선
<그림 2. 2차 베지에 곡선>

3개의 조절점 A, B, C를 이용해 그린 두 개의 직선 즉, 두 개의 1차 베지에 곡선이 있다. 그리고 이 곡선에서 보간되는 점 E와 F도 있다. 이때 점 E와 F를 이용해 또 다른 직선을 그릴 수 있고 이 직선에서 보간되는 점 P도 추가할 수 있다. 이제 점 E와 F 그리고 P를 보간하면 P의 행적이 곡선을 만들어 낸다(이해가 되지 않는다면 「중학생도 알 수 있는 베지에 곡선」을 참고한다).

그럼 이제 2차 베지에 곡선을 직접 그려보도록 하자. 선분에서 블렌딩 되는 점 P를 구하는 공식은 다음과 같다(자세한 내용은 「프런트엔드 엔지니어를 위한 베지에 곡선(Bézier Curves) - 2편」을 참고). 이때 s = 1 - t다.

P = (s * A) + (t * B)

그림 2를 보면 알 수 있듯이 2차 베지에 곡선을 그리기 위해서는 점 E와 F 그리고 P를 보간해야 한다. 점 E는 조절점 A와 B를 이용해 구할 수 있고, 점 F는 조절점 B와 C를 이용해 구할 수 있다. 그리고 점 P는 다시 점 E와 F를 이용해 구할 수 있다.

E = (s * A) + (t * B)
F = (s * B) + (t * C)
P = (s * E) + (t * F)

이 공식을 자바스크립트 코드로 옮겨보자. 여기에서는 구현에 있어 몇 가지 중요한 함수만 소개한다. 전체 코드는 코드펜(CodePen)에 작성해 놓은 예제를 참고한다.

먼저 blender()는 점 A와 점 B 그리고 가중치 t를 전달받아 블랜딩한 결괏값을 반환하는 함수다.

function blender(A, B, t) {
    if (t === 0) {
        return A;
    }

    if (t === 1) {
        return B;
    }

    return ((1 - t) * A) + (t * B); // or A + t * (B - A)
}

이때 blender()는 좌표 하나에 대한 연산만 책임지므로 x, y 좌표를 연산하기 위해 blend()를 작성한다.

function blend(x1, x2, y1, y2, t) {
  const x = blender(x1, x2, t);
  const y = blender(y1, y2, t);

  return {x, y};
}

다음으로 blend()를 이용해 점 A와 점 B의 좌표를 전달해 점 E의 좌푯값을 구하고 점 B와 점 C의 좌표를 전달해 점 F의 좌표를 구한다. 그리고 다시 점 E와 점 F의 좌표를 전달해 점 P의 좌표를 구하는 방식으로 공식을 구현한다.

interpolateBtn.addEventListener('click', function() {
  // Start the interpolation.
  raf(function(t) {
    const posE = blend(posA.x, posB.x, posA.y, posB.y, t);
    const posF = blend(posB.x, posC.x, posB.y, posC.y, t);
    const posP = blend(posE.x, posF.x, posE.y, posF.y, t);
    ...
  }, 1000);
});

아래 데모를 실행해 보자. 점 P가 보간되면서 그려진 곡선을 2차 베지에 곡선이라고 한다.

See the Pen qrpYwj by Uyeong Ju (@uyeong) on CodePen.

수식 정리

우리가 2차 베지에 곡선을 위해 사용한 수식은 다음과 같다.

E = (s * A) + (t * B)
F = (s * B) + (t * C)
P = (s * E) + (t * F)

하지만 이 수식은 조금 장황하며 자바스크립트 코드상에서도 함수 호출이 빈번한 상태다. 이 수식을 방정식으로 좀더 간결하고 효율적으로 표현할 수 있다. 일단 연산식에 있는 괄호를 없애고 좀더 간략하게 수식을 표현한다.

E(t) = sA + tB
F(t) = sB + tC
P(t) = sE(t) + tF(t)

이번엔 중학생 때 배워본 몇 가지 곱셈 공식 사용하여 세 줄로 표현한 수식을 한 줄로 작성하고 이차방정식으로 정리한다.

P(t) = s(sA + tB) + t(sB + tC)
P(t) = (s²)A + (st)B + (st)B + (t²)C
P(t) = (s²)A + 2(st)B + (t²)C

자, 몇 가지 규칙을 더 추가하자. t가 0이라면 P는 항상 A와 같으며 다음과 같이 증명할 수 있다.

P(t) = (s²)A + 2(st)B + (t²)C
P(t) = (1²)A + 2(1 * 0)B + (0²)C
P(t) = (1)A + 2(0)B + (0)C
P(t) = (1)A
P(t) = A

다시 t가 1이라면 P는 항상 C와 같으며 다음과 같이 증명할 수 있다.

P(t) = (s²)A + 2(st)B + (t²)C
P(t) = (0²)A + 2(0 * 1)B + (1²)C
P(t) = (0)A + 2(0)B + (1)C
P(t) = (1)C
P(t) = C

이제 정리한 수식을 자바스크립트로 작성해보자. 함수명은 quadBezier로 짓고 2차 베지에 곡선임을 나타낸다.

function quadBezier(A, B, C, t) {
  if (t === 0) {
    return A;
  }
  
  if (t === 1) {
    return C;
  }
  
  const s = 1 - t;
  
  return Math.pow(s, 2) * A + 2 * (s * t) * B + Math.pow(t, 2) * C;
}

이렇게 작성한 함수는 다음과 같이 사용할 수 있다.

interpolateBtn.addEventListener('click', function() {
  // Start the interpolation.
  raf(function(t) {
    const x = quadBezier(posA.x, posB.x, posC.x, t);
    const y = quadBezier(posA.y, posB.y, posC.y, t);
    ...
  }, 1000);
});

여기까지 1차 베지에 곡선과 2차 베지에 곡선에 대해 알아봤다. 다음 편에서는 3차 베지에 곡선을 소개하고 이 곡선을 애니메이션에서 어떻게 활용하는지 소개하겠다.

참고