2022.07

Cardinal Spline Algorithm 적용하여 Canvas API 사용한 Drawing 기능 개발

Spline Tool Preview

Spline Tool Preview

알고리즘 선택 이유

요구사항에 맞는 Algorithm 중 에서는 (Lagrange, Cardinal) Interpolating 두가지로 좁혀졌다.

초기에는 Lagrange 보간법을 사용했으나, Point 가 좁고 많아질수록 보간값이 불안정하여 곡선이 넓게 튀는 현상이 발견되어, 비교적 Point 가 많아도 안정적인 Cardinal 보간법으로 적용하였다.

Lagrange Interpolating Spline

Lagrange Interpolating Spline

Cardinal Interpolating Spline

Cardinal Interpolating Spline

구현

Konva 라이브러리 에서는 커스텀 드로잉 기능을 구현할 수 있게 sceneFunc 라는 함수를 제공한다.

이 함수 내에서 Canvas Context 저수준 객체를 가져와서 직접 알고리즘으로 드로잉하여 Scene 을 생성한다.

사용자가 곡선을 선택하고, 선뿐만이 아닌 영역을 그리기 위해 반복적인 드로잉이 필요하게 되어, 정방향, 역방향 드로잉을 연속적으로 수행하는 한붓그리기 방식으로 영역 생성을 위한 드로잉을 수행하였다.

// **useSpline.ts -** drawSpace

// Spline Space SceneFunc
// calcInterpolatingPoint(X|Y) 함수는 core algorithm 입니다.
function drawSpace(ctx: Context, shape: Shape<ShapeConfig>, hit: boolean) {
    let scPx = new Float64Array(n2),
      scPy = new Float64Array(n2);
    let X, Y;

    // Process:
    // 1. 첫점에서 너비만큼 X축을 벌려 왼쪽부터 같은 곡선을 그립니다.
    // 2. 왼쪽 선에서 선을 이으면서 오른쪽도 같은 곡선을 거꾸로 그리면서 한붓 그리기를 합니다.
    // 3. 완성된 선을 지정된 색으로 채웁니다.

    if (n > 2) {
      for (let dir of [-1, 1]) {
        for (let i = 0; i < n; i++) {
          X = scPx[i + 1] = Px[i] + dir * infos[i]?.weight;
          Y = scPy[i + 1] = Py[i];
        }

        // [0, last point] not draw, only using by algorithm
				initPoint(scPx, scPy)

        if (dir === -1) {
          ctx.beginPath();
        }

        if (dir === 1) {
          scPx = scPx.reverse();
          scPy = scPy.reverse();
        }

        for (let i = 1; i < n; i++) {
          for (let k = 0; k < 26; k++) {
            X = calcInterpolatingPointX(scPx, scPy);
            Y = calcInterpolatingPointY(scPx, scPy);

            ctx.lineTo(X, Y);
          }
        }

        if (dir === 1) {
          ctx.closePath();

          if (hit) {
            // 충돌 영역은 shape 로 그려야 합니다. (화면 표시 안됨, 클릭시 영역 반응)
            ctx.fillStrokeShape(shape);
          } else {
            // gradient 는 shape 로 채우기 불가능하여 raw api 를 사용합니다.
            ctx._context.fillStyle = _getGradientSetting(ctx);
            ctx._context.fill();
          }
        }
      }
    }
  }