Canvas 를 이용한 삼각함수 이해 02

삼각함수 응용

앞서 정리한 내용을 바탕으로 물체의 회전에 적용되는 행렬을 정리해 보고자 합니다.
먼저 몇가지 사항을 확인해 보겠습니다.
원점 0,0 에서 반지름 R(구체적으로 1이라 하겠습니다.)로 시작하는 각을(theta) 0 ~ 360 도로 증가하면서 좌표 ( cos(theta), sin(theta) ) 를 연결하면 원을 그릴 수 있습니다. ( 그림 - 소스는 하단에 설명 )

원형

두점 사이의 거리

그림에서 O 와 P 의 거리를 (선분 OP) 는 반지름이 1 이라면, 알파(alpha) 가 45도 라고 가정할 때 1 * Math.cos( Math.PI * 45 / 180 ) = x, 1 * Math.sin( Math.PI * 45 / 180 ) = y, 의 값이 나옵니다.
빗변의길이 = sqrt(x^2 + y^2) 이니까 반지름이 1이면 당연히 길이는 1이 나오게 됩니다. x = 0.7071… , y = 0.7071 … 만약 반지름을 10 이라고 하면 길이는 10이고, 좌표는 x, y 모두 7.071.. 이 될 것입니다.
그럼 선분 PQ 의 거리는 어떻게 될까요? 현재 예제에서는 y 가 같기 때문에 차이가 0이라 계산에 영향을 주진 않겠지만, 조금 도식화 하면 sqrt( (x2-x1)^2 + (y2-y1)^2 ) , 각 좌표의 차이의 제곱을 더해서 루트로 씌우면 두 선분의 거리가 됩니다. 빗변이 1이라고 가정했으니 조금 단순화 해서 적어 보면 x = cos(theta), y = sin(theta) 입니다. 그림의 선분 PQ 를 기준으로 하면 다음과 같이 생각해 볼 수 있습니다. ( 빗변이 1 ) Q 점은 ( cos(theta), sin(theta)), P 점은 ( cos(alpha), sin(alpha)) 입니다. 다시 선분 PQ 의 제곱은 = (cos(theta) - cos(alpha)) ^ 2 + (sin(theta)-sin(alpha))^2 입니다.

cos(theta)^2 + sin(theta) ^2 = 1 입니다.

그림에서 보이는 원은 cos(theta), sin(theta) - x, y 의 조합입니다.( 여기서 theta 는 각을 의미함 )
두점 사이의 거리에서 보았듯이, 모든 빗변은 1입니다. 1의 제곱으로 이야기 할 수 있습니다. 빗변 제곱 = x거리 제곱 + y거리 제곱과 같습니다. x 거리는 cos(theta) 입니다. y 거리는 sin(theta) 입니다.
1 = sin^2 + cos^2 이 쉽게 연상 될 수 있을 것입니다. 빗변의 길이가 1일 때 sin^2 + cos^2 = 1 은 원에서 보듯 언제나 1 입니다.

그림에서 선분 PQ 의 길이를 어떻게 구할까요?

  1. 구하고자 하는 길이는 선분 PQ 이고, 삼각형 OPQ 를 대상으로 하지만, 이해를 위해 삼각형 OPT 를 기준으로 생각해 보겠습니다.

alpah 움직인 위치의 P 점에서 beta 만큼 움직인 Q 점으로의 이동을 생각해 보려고 합니다.
직관적인 이해는 OPT 삼각형으로 이해하는게 좋을 것 같아서 해당 예시로 정리하겠습니다.

  1. TP(PT) 의 길이를 구하는 방법

주어진 정보는 alpha 가 45도 OP 의 길이가 1, OT 의 길이가 1 입니다.
점선으로 그려진 PwtT 의 삼각형을 이용하면 Pwt 의 길이, wtT 의 길이를 각각 제곱하면 PT 선분의 제곱과 같습니다.( 피타고라스 )

  1. 높이 구하기 ( Pwt )

주어진 정보 sin(alpha) * 1 (OP 의 길이) 이면 Pwt 의 길이를 알 수 있습니다. 이를 h 라고 하겠습니다.

  1. 가로 구하기 ( wt 에서 T 까지 , wtT )

주어진 정보에서 cos(alpha) * 1 하면 O에서 wt 까지의 길이를 알 수 있습니다. 이를 preW 라고 하겠습니다.
wt 에서 T 까지의 길이는 OT 의 길이에서 preW 를 뺀 값과 같습니다. 이를 w 라고 하겠습니다.
w = 1 - preW 입니다. 풀이하면 w = (1 - cos(theta)) 입니다.

  1. 그럼 PT 길이의 제곱은 어떻게 될까요 ? ( 피타고라스 ) - PT^2 라고 하겠습니다.

PT^2 = h^2 + w^2 , PT^2 = sin(alpha)^2 + ( 1 - cos(alpha))^2
PT^2 = sin(alpha)^2 + 1 - 2cos(alpha) + cos(alpha) ^2 PT^2 = (sin(alpha)^2 + cos(alpha)^2) + 1 - 2cos(alpha), PT^2 = 1 + 1 - 2* cos(alpha)

  1. 최종 구하고자 하는 값이 theta(beta) 입니다. 이를 alpha 에서 beta 로 변경합니다.

PQ^2 = 1 + 1 - 2 * cos(beta) 입니다. beta 가 theta - alpha 라는 것을 기억해 둡니다.

두점 사이의 거리로 구한 선분 PQ 의 길이

  1. 앞서 언급한 내용을 간단한 형태로 다시 정리해 보겠습니다.

PQ^2 = (cos(theta) - cos(alpha)) ^ 2 + (sin(theta)-sin(alpha))^2 , beta 가 아닌 theta 인 이유는 Q의 점이 오른쪽 0~T 의 축으로 부터 시작되기 때문입니다.

  1. 식의 전개 - 확인을 위해서 어쩔 수 없이 전개 합니다. ( ^^ )

PQ^2 = cos(theta)^2 - 2*cos(theta)cos(alpha) + cos(alpha)^2 + sin(theta)^2 - 2sin(theta)*sin(alpha) + sin(alpha)^2 ( sin^2 + cos^2 = 1 에 해당하는 부분 정리 )
PQ^2 = (cos(theta)^2 + sin(theta)^2) + (cos(alpha)^2 + sin(alpha)^2 ) - 2 *(cos(theta)*cos(alpha) + sin(theta)sin(alpha))
PQ^2 = 1 + 1 - 2
(cos(theta)*cos(alpha) + sin(theta) * sin(alpha)) 입니다.
위의 전개된 내용과 아주 유사하네요 ….

지금까지의 정리

  1. PQ^2 이 같기 때문에 두개를 연결해 보겠습니다.

1 + 1 - 2 * cos(beta) = 1 + 1 - 2*(cos(theta)*cos(alpha) + sin(theta) * sin(alpha))

  1. 양변에서 같은 것을 제거해 보겠습니다.

cos(beta) = cos(theta)*cos(alpha) + sin(theta)*sin(alpha)
앞서 언급한 것과 같이 beta = theta - alpha 입니다. 그리고 sin(-a) = -sin(a) 입니다. 부호가 변경됩니다.
cos(theta-alpha) = cos(theta)*cos(alpha) + sin(theta)*sin(alpha) 코사인 덧셈법칙 입니다.

  1. 이제 Q.x 에 대해서는 정리할 수 있습니다.

최종적으로 보면 cos(alpha) 는 P점의 x 값입니다. , sin(alpha) 는 P 점의 y 값입니다.
결국 Q점의 x 값은 cos(beta+alpha) = cos(beta)*cos(alpha) - sin(beta)*sin(alpha) 라고 이야기 할 수 있습니다.
Q.x = cos(beta)*P.x - sin(beta) * P.y 입니다.
[cos, -sin] 을 P.x, P.y 와 곱해서 더해주면 Q.x 의 위치를 알 수 있다는 의미 입니다.

Q.y 는 sin 덧셈을 활용하면 구할 수 있습니다.

  1. 위 그림 OPT 삼각형을 기준으로 간단하게 살펴 보겠습니다.

P 점을 중심으로 각도 theta1 이 있고 P점에서 수직으로 wt 연결할 때 alpha1, beta1 각이 나눠진 것으로 간주 하겠습니다.
P 점 각 theta1 = alpha1 + beta1 이고 alpah1 과 beta 1은 P에서 마주 보는 변과 수직으로 나뉜 각도라 가정하겠습니다. PO 의 길이를 a 라고 하고 PT 의 길이를 b 라고 하겠습니다. Pwt 의 길이를 c 라고 정의 합니다.
알고 있는 것은 P의 각 theta1 = alpha1 + beta1 변의 길이 a, b, 높이 c 라고 정의 합니다.

  1. 전체 삼각형의 넓이(면적)

알고 있는 정보를 바탕으로 sin(theta1) * a 라고 하면 O에서 PT 선분에 직각인 높이를 알게 됩니다. 이를 h 라고 하겠습니다.
선분 PT 의 길이는 b 라고 주어 졌으니, 면적은 0.5 * h * b = 0.5 * sin(theta1) * a * b 입니다.

  1. 삼각형 PQwt의 넓이(면적)

OPwt 의 P 위치에서 각도를 alpha1 이라고 정의 합니다. cos(alpha1) * a = Pwt 까지의 길이 입니다. 앞서 정의한 c 입니다.
sin(alpha1) * c = wt 에서 선분 OP 와 수직을 이루는 높이 입니다. h1 이라고 하겠습니다.
넓이는 0.5 * c * h1 = 0.5 * sin (alpha1) * c * a = 0.5 * sin(alpha1) * cos(alpha1) * a * a 입니다.
치환을 위해서 넓이는 0.5 * sin(alpha1) * a * c 로 정의해 놓습니다.

  1. 삼각형 PwtT의 넓이(면적)

wtPT 의 P 위치에서의 각도를 beta1 이라고 정의 합니다. cos(beta1)*b = Pwt, P에서 wt 까지의 길이 c 입니다. 이 c 는 앞서 계산한 c 와 같습니다. c = cos(beta1)*b = cos(alpha1) * a sin(beta1)*c = wt 에서 선분 PT 와 수직을 이루는 높이 입니다. h2 라고 정의 하겠습니다. 넓이는 0.5 * b * h2 = 0.5 * sin(beta1) * b * c c를 분배하지 않고 일단 그대로 두겠습니다.

  1. 계산을 원활히 하기 위해 공통적인 영역을 바꿔 보겠습니다.

정의한 내용중 c = cos(beta1) * b = cos(alpha1) * a 이 항목을 활용하여 위의 두식을 약간 변형해 보겠습니다.
삼각형 PQwt 면적 0.5 * sin(alpha1) * a * c = 0.5 * sin(alpha1) * a * cos(beta1) * b
삼각형 PwtT 면적 0.5 * sin(beta1) * b * c = 0.5 * sin(beta1) * b * cos(alpha1) * a

  1. 전체 삼각형의 넓이는 부분삼각형의 넓이의 합과 같습니다.

0.5 * sin(theta1) * a * b = 0.5 * sin(alpha1) * a * cos(beta1) * b + 0.5 * sin(beta1) * b * cos(alpha1) * a
양변에서 같은 내용을 나누어 삭제해 보겠습니다. 같은 항목인 ( 0.5, a, b 를 각각 양변에서 나누어 줍니다. )
sin(theta1) = sin(alpha1)*cos(beta1) + sin(beta1)*cos(alpha1)
tehta1 = alpha1 + beta1 입니다.
sin(alpha1+beta1) = sin(alpha1)*cos(beta1) + sin(beta1)*cos(alpha1)
cos(alpha1) = x, sin(alpha1) = y 주어진 x,y 가 있을 경우 변화한 beta1 만큼만 이동하면 됩니다.
어떤 좌표에서 beta1 이 주어지면 beta1 만클이 아닌 시작점부터 변환한 것이기 때문에 변환된 위치는 사실 alpha1+beta1 의 위치 입니다.
y * cos(beta1) + x * sin(beta1) 이므로 행렬식에는 [ sin, cos ] 으로 표혀하게 됩니다.

이상의 x, y 를 모두 조합하면 다음과 같은 행렬이 구성됩니다.

$$ \begin{bmatrix}cos&-sin\\sin&cos \end{bmatrix} \begin{bmatrix}x\\y \end{bmatrix} $$

Canvas 예제

소스 입니다.

Canvas에 대한 정리도 필요한데, 유도 과정을 이야기 하다 보니, 글이 너무 길어져서 그림을 어떻게 그렸는지만
참조 할 수 있도록 하겠습니다.
말씀 드린데로 원은 반지름을 cos(theta) , sin(theta) 에 곱해서 x, y 를 구성하였습니다.
중심점으로 translate 하고 진행하였습니다. 좌표를 쉽게 보기 위해서 입니다.
Canvas 는 앞서도 언급한 것처럼 왼쪽 상단이 0, 0 입니다. y 좌표를 뒤집은 이유입니다.


     function makeCircleUI(width, height) {
         const gaps = 30;
         const fGap = 20;
         const radius = width/2 - gaps*2;

         const canvas = makeCanvas(width, height);
         const ctx = canvas.getContext("2d");
         ctx.clearRect(0,0,width,height);
         ctx.save();

         ctx.translate(width/2,height/2); //new 0,0 base

         ctx.save();
         ctx.beginPath();

         
         ctx.font = "bold 18px consolas";
         ctx.textAlign = "center";
         ctx.textBaseline = "middle";

         ctx.fillStyle = "#EEEE22";
         ctx.fillStyle = "#000000";
         ctx.strokeStyle = "#000080";
         ctx.lineWidth = 1;
         ctx.setLineDash([2, 2]);

         ctx.moveTo(-radius,0);
         ctx.lineTo(radius,0);
         ctx.moveTo(0,-radius);
         ctx.lineTo(0,radius);

         ctx.moveTo(radius,0);
         ctx.lineTo( Math.cos(Math.PI*0.25)*radius, -Math.cos(Math.PI*0.25)*radius);

         ctx.moveTo( Math.cos(Math.PI*0.25)*radius, -Math.cos(Math.PI*0.25)*radius );
         ctx.lineTo( Math.cos(Math.PI*0.25)*radius, 0);

         ctx.moveTo( Math.cos(Math.PI*0.25)*radius-10, 0);
         ctx.lineTo( Math.cos(Math.PI*0.25)*radius-10, -10);            
         ctx.lineTo( Math.cos(Math.PI*0.25)*radius+10, -10);                        
         ctx.lineTo( Math.cos(Math.PI*0.25)*radius+10, 0);                                    

         ctx.fillText("wt", Math.cos(Math.PI*0.25)*radius, 15 );

         ctx.stroke();
         ctx.closePath();
         ctx.restore();

         ctx.font = "bold 18px consolas";
         ctx.textAlign = "center";
         ctx.textBaseline = "middle";

         ctx.beginPath();
         ctx.fillStyle = "blue";
         ctx.moveTo(0,0);
         ctx.arc(0,0, 50, 0, -Math.PI*0.25, true);
         ctx.lineTo(0,0);
         ctx.fill();
         ctx.fillStyle = "#EFEFEF";
         ctx.fillText("α", Math.cos(Math.PI*22.5/180)*30, -Math.sin(Math.PI*22.5/180)*30); 
         ctx.closePath();

         ctx.beginPath();
         ctx.fillStyle = "red";
         ctx.moveTo(0,0);
         ctx.arc(0,0, 50, -Math.PI*0.25, -Math.PI*0.5,  true);
         ctx.lineTo(0,0);
         ctx.fill();

         ctx.fillStyle = "#EFEFEF";
         ctx.fillText("β", Math.cos(Math.PI*0.37)*30, -Math.sin(Math.PI*90/360)*30); 

         ctx.closePath();

         ctx.beginPath();
         ctx.fillStyle = "rgba(255,255,0,0.2)";
         ctx.strokeStyle = "#000080";
         ctx.lineWidth = 1;
         ctx.moveTo(0,0);
         ctx.arc(0,0, 65, 0, -Math.PI*0.5,  true);
         ctx.lineTo(0,0);
         ctx.fill();

         ctx.fillStyle = "#222222";
         ctx.fillText("θ", Math.cos(Math.PI*100/360)*75, -Math.sin(Math.PI*100/360)*75);             
         ctx.stroke();
         ctx.closePath();


         ctx.beginPath();

         ctx.fillStyle = "#EEEE22";
         ctx.fillStyle = "#000000";
         ctx.strokeStyle = "#000080";
         ctx.lineWidth = 2;

         ctx.font = "bold 18px consolas";
         ctx.textAlign = "center";
         ctx.textBaseline = "middle";

         ctx.fillText("0(0,0)", 40,20);

         const points = [{x:0,y:0}];

         for ( let i = 0; i < 360; i++ ) {
             let theta = i*Math.PI/180;
             let sx  = Math.cos(theta);;
             let sy = -Math.sin(theta); 
             let tx = radius*sx;
             let ty = radius*sy;


             if ( i == 0 ) {
                 ctx.moveTo(tx,ty);
             } else {
                 ctx.lineTo(tx,ty);
             }
             if ( i % 45 == 0 ) {
                 let txt = i;
                 if ( i == 45 ) {
                     txt = "P (45)"
                     points.push({x:tx,y:ty});
                 } else if ( i == 90 ) {
                     txt = "Q (90)"
                     points.push({x:tx,y:ty});
                 } else if ( i == 0 ) {
                     txt = "  T(0)";
                 }
                 ctx.fillText( txt , tx+fGap*sx,ty+fGap*sy);
             }
         }

         ctx.closePath();
         ctx.stroke();

         ctx.fillStyle = "#FF2233";
         ctx.strokeStyle = "#FF33FF";
         ctx.lineWidth = 5;
         ctx.lineJoin = "round";

         ctx.beginPath();
         for ( let i = 0; i < points.length; i++ ) {
             if ( i == 0 )
                 ctx.moveTo(points[i].x, points[i].y);
             ctx.lineTo(points[i].x,points[i].y);
         }

         ctx.closePath();
         ctx.stroke();

         ctx.beginPath();
         for ( let i = 0; i < points.length; i++ ) {
             ctx.moveTo(points[i].x, points[i].y);
             ctx.arc(points[i].x,points[i].y,10,0,Math.PI*2);
         }
         ctx.fill();
         ctx.closePath();

         ctx.restore();

         document.body.appendChild(canvas);
         console.log(canvas.toDataURL());
     }

 Share!