Canvas Bezier / Quadratic Curve

Franklee·2023년 3월 5일
0

normal

목록 보기
3/5
post-thumbnail

Quadratic (이차 곡선)

quadraticCurveTo(cp1x, cp1y, x, y)

  • cp1x 및 cp1y로 지정된 제어점을 사용하여 현재 펜의 위치에서 x와 y로 지정된 끝점까지 이차 베지어 곡선을 그립니다.

quadraticCurveTo(cp1x, cp1y, x, y)에서
cp1x, cp1y 로 위 그림의 Control Point를 지정하고,
x,y 로 Ending Point를 지정할 수 있다.

Bezier (베지어 곡선)

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

  • (cp1x, cp1y) 및 (cp2x, cp2y)로 지정된 제어점을 사용하여 현재 펜 위치에서 x 및 y로 지정된 끝점까지 삼차 베지어 곡선을 그립니다.

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)에서
cp1x, cp1y, cp2x, cp2y 를 그림의 각 Control Point와 같이 곡선 지점을 설정 해 줄 수 있다.

베지어 곡선을 사용한 데이터 표현

먼저, 하고자 하는 목표는 데이터를 사용해 형태에 맞게 곡선그래프를 Canvas에 그려보는 것이다.

📝 문제

  1. 데이터를 입력받아 해당 데이터를 그래프와 같은 형태로 표현하는것.
  2. 그래프의 선의 형태는 직선이 아닌 곡선 형태로의 표현.
  3. 모든 데이터를 전부 하나하나 표시하기보다 전체적인 흐름을 보여주고자 함.
  4. 보다 더 세밀한 곡선을 표시하기 위해 bezierCurveTo()를 사용.

🔍 이유

  1. 데이터를 통해 기업의 성장률 혹은 사업의 매출 등을 표현하기 위함.
  2. 단순 이미지 혹은 .GIF를 사용하여 표현하는 것이 아닌 코드를 통해 구현함으로서 입력 데이터에 따라 유기적으로 변형되어 표시될 수 있는 UI를 제공하기 위함.
  3. 코드를 통해 구현하였기에 JS / CSS 애니메이션을 통해 보다 더 역동적인 표현이 가능한 장점을 가지고 있음.
  4. 재사용성

💡 해결방안

1. 베이어 곡선을 사용한 기본적인 이해 및 학습

ctx.beginPath(); // 경로 생성
Ctx.moveTo(0,0); // 경로 시작점 정의

ctx.bezierCurveTo(10, 100, 30, 100, 40, 0);

ctx.stroke(); // 실제 그리기


(이미지1)
그림에서의 상단 두개의 점이 각각 cp1x,cp1y / cp2x,cp2y 를 지정한다.
(실제로는 보다 상단에 위치)
초록색 점은 끝지점인 40,0(x,y)이다.

여기서 실제 y-Point를 100으로 지정했지만, 실제 곡선의 최정상 지점은 100이 아니다.

(이미지2)
위 이미지를 보면 곡선은 bezierCurveTo(10, 100, 30, 100, 40, 0);으로 지정했지만 옆에 직선 lineTo(40, 100); 과의 높이차이가 보인다.
즉, 실제 데이터가 표시한 지점까지 정확하게 표시하기 위해서는 베지어곡선의 cp1y / cp2y실제 위치보다 높게 표시해야 될 것으로 판단된다.

두개의 베지어 곡선을 이어 그려본다.

ctx.beginPath();
ctx.moveTo(0, 100);

ctx.bezierCurveTo(10, 200, 30, 200, 40, 100);
ctx.bezierCurveTo(50, 0, 70, 0, 80, 100);

ctx.stroke();


(이미지3)
여기서 알 수 있는 점은 최상단지점과 최하단 지점에 대해, 두개의 곡선을 단순 이어 붙이기를 하면 의도와는 다르게 표현 될 수 있다.


(이미지4)
위 이미지와 같이 곡선 그릴시 두번째 지점까지 설정을 해야하는데 여기서 두번째 지점에 대해 다시 곡선을 그리면 이미지3 처럼 될 수도 있기에 이미지4 에서 1 과 2 사이의 중간지점을 곡선 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)의 x,y로 설정하면 해결 될수 있다고 본다.


2. 데이터 가공

앞서 말했듯이 데이터의 모든 포인트를 표시하기보다 전체적인 상승 하강의 표현을 목적으로 하기에 데이터를 가공할 필요가 있다.

📝 문제

  • 여러 데이터에서 상승 및 하강 포인트 도출

🔍 이유

  • 전체적인 흐름을 표시하기 위함
  • 예를 들어, y값이 10단위로 상승된다면 각각의 곡선을 그리기에는 불필요하다고 생각되기때문이다.

💡 해결방안

  • 배열을 통해 데이터를 입력받고 상승/하강의 흐름이 바뀌기 전까지 곡선포인트를 받지 않는다.
  • 예를 들어, [10,20,40,20,60,100]의 배열이 있다면 40까지 지속적으로 상승하고 이후에 하락함으로, 40을 새로운 배열에 넣고, 20 또한 하강의 시작점이기도 하지만 끝점이기도 하기 때문에 20을 넣는다. 결과적으로 [40,20,100]의 배열을 생성하여 이를 토대로 그래프를 그린다.
  const seperatePoint = (): number[] => {
    //  여러개의 점을 유연하게 표시하기 위해서 상승점과 하강점만을 추출
    const point: number[] = []; //  결과점 배열
    let temp = arr[0]; // 비교 값
    let dir = 'up'; //  이전까지의 흐름이 상승인지, 하강인지 표시 (시작 상승)
    for (let i = 1; i < arr.length; i += 1) {
      if (temp > arr[i]) {
        //  이전값이 arr[i]보다 클 때
        if (dir === 'up') {
          //  흐름은 상승이지만 하강으로의 변곡점이 나왔기에
          point.push(temp); //  최고상승점을 point배열에 저장
          dir = 'down'; //  하강 흐름으로 변경
        }
        temp = arr[i]; // 비굣값 temp에 할당
      }

      if (temp < arr[i]) {
        //  아전값이 arr[i]보다 작을 때
        if (dir === 'down') {
          //  흐름이 하강이면 (이전까지는 상승)
          point.push(temp); //  최저 하강점을 point배열에 저장
          dir = 'up'; //  상승 흐름으로 변경
        }
        temp = arr[i]; // 비굣값 temp에 할당
      }

      if (temp === arr[i]) {
        //  값이 같다면 비굣값만 변경
        temp = arr[i];
      }

      if (i + 1 === arr.length) {
        //  다음 인덱스가 없다면 마지막값 point배열에 저장
        point.push(temp);
      }
    }
    return point;
  };

3. 곡선 포인트 잡기

  1. 각 곡선의 넓이는 40
  2. 곡선과 곡선의 중간지점은 (point[n]+point[n+1]) / 2
//x 값은 시작지점 = 0, cp1x = 10, cp2x = 30, x = 40 설정(곡선점은 = 30)
//cpy 값은 +20 (이미지2에 의한 대체 수)
// y = (point[n]+point[n+1]) / 2

ctx.bezierCurveTo(x, y + 20, x + 20, y + 20, x + 30, (point[n]+point[n+1]) / 2);
const drawCurve = (
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    z: number,
    bool: boolean
  ) => {
    if (bool) {
      ctx.bezierCurveTo(x, y + 20, x + 20, y + 20, x + 30, z);
      //  상승곡선
    } else {
      ctx.bezierCurveTo(x, y - 20, x + 20, y - 20, x + 30, z);
      //  하강곡선
    }
};

  const curve = () => {
    const ctx = ref.current?.getContext('2d');
    if (ctx) {
      ctx.beginPath();
      ctx.moveTo(0, 0);
      if (ctx !== undefined && ctx !== null) {
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 2;
      }
      const point = seperatePoint(); // 여러점들에서 고점, 저점 point만을 모은 배열
      let xPoint = 10; // 초기 x값

      for (let i = 0; i < point.length; i += 1) {
        const yPoint = point[i]; // 현재 포인트
        const nextYPoint = point[i + 1]; // 다음 포인트 (중간점 찾기위해)
        if (i % 2 === 0) {
          //  상승곡선 그리기
          if (i + 1 === point.length) {
            //  마지막 요소이라면
            drawCurve(ctx, xPoint, yPoint, yPoint, true);
          } else {
            drawCurve(ctx, xPoint, yPoint, (yPoint + nextYPoint) / 2, true);
          }
        } else if (i % 2 === 1) {
          //  하강곡선 그리기
          if (i + 1 === point.length) {
            //  마지막 요소이라면
            drawCurve(ctx, xPoint, yPoint, yPoint, false);
          } else {
            drawCurve(ctx, xPoint, yPoint, (yPoint + nextYPoint) / 2, false);
          }
        }
        xPoint += 40;
        //  다음 곡선의 시작포인트 설정
      }
      ctx.stroke();
    }
  };

4. 그래프 그리기

  • 위 코드들을 통해 [10, 20, 30, 60, 40, 20, 60, 80, 60, 100] 데이터에 대한 그래프를 그려본다.

⭕️ 정상적으로 그래프가 그려진것을 확인 할 수 있다.

🛠️ 다만, 몇가지 수정점이 필요하다.

  • 예시로 사용한 y점의 +20이 아닌 데이터에 따라 정확한 수치로의 표시가 가능하도록 해야한다.
  • 상승흐름이 길어짐에 따라 x(곡선의 넓이)도 넓게 표시하도록 해야한다.
  • 곡선 그래프가 그려지는 것에 애니메이션을 적용한다.

다음 Canvas 포스팅에는 애니메이션 적용에 대해 작성하도록 하겠다.

profile
Frontend Dev

0개의 댓글