삼각함수 응용
앞서 정리한 내용을 바탕으로 물체의 회전에 적용되는 행렬을 정리해 보고자 합니다.
먼저 몇가지 사항을 확인해 보겠습니다.
원점 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 의 길이를 어떻게 구할까요?
- 구하고자 하는 길이는 선분 PQ 이고, 삼각형 OPQ 를 대상으로 하지만, 이해를 위해 삼각형 OPT 를 기준으로 생각해 보겠습니다.
alpah 움직인 위치의 P 점에서 beta 만큼 움직인 Q 점으로의 이동을 생각해 보려고 합니다.
직관적인 이해는 OPT 삼각형으로 이해하는게 좋을 것 같아서 해당 예시로 정리하겠습니다.
- TP(PT) 의 길이를 구하는 방법
주어진 정보는 alpha 가 45도 OP 의 길이가 1, OT 의 길이가 1 입니다.
점선으로 그려진 PwtT 의 삼각형을 이용하면 Pwt 의 길이, wtT 의 길이를 각각 제곱하면 PT 선분의 제곱과 같습니다.( 피타고라스 )
- 높이 구하기 ( Pwt )
주어진 정보 sin(alpha) * 1 (OP 의 길이) 이면 Pwt 의 길이를 알 수 있습니다. 이를 h 라고 하겠습니다.
- 가로 구하기 ( wt 에서 T 까지 , wtT )
주어진 정보에서 cos(alpha) * 1 하면 O에서 wt 까지의 길이를 알 수 있습니다. 이를 preW 라고 하겠습니다.
wt 에서 T 까지의 길이는 OT 의 길이에서 preW 를 뺀 값과 같습니다. 이를 w 라고 하겠습니다.
w = 1 - preW 입니다. 풀이하면 w = (1 - cos(theta)) 입니다.
- 그럼 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)
- 최종 구하고자 하는 값이 theta(beta) 입니다. 이를 alpha 에서 beta 로 변경합니다.
PQ^2 = 1 + 1 - 2 * cos(beta) 입니다. beta 가 theta - alpha 라는 것을 기억해 둡니다.
두점 사이의 거리로 구한 선분 PQ 의 길이
- 앞서 언급한 내용을 간단한 형태로 다시 정리해 보겠습니다.
PQ^2 = (cos(theta) - cos(alpha)) ^ 2 + (sin(theta)-sin(alpha))^2 , beta 가 아닌 theta 인 이유는 Q의 점이 오른쪽 0~T 의 축으로 부터 시작되기 때문입니다.
- 식의 전개 - 확인을 위해서 어쩔 수 없이 전개 합니다. ( ^^ )
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)) 입니다.
위의 전개된 내용과 아주 유사하네요 ….
지금까지의 정리
- PQ^2 이 같기 때문에 두개를 연결해 보겠습니다.
1 + 1 - 2 * cos(beta) = 1 + 1 - 2*(cos(theta)*cos(alpha) + sin(theta) * sin(alpha))
- 양변에서 같은 것을 제거해 보겠습니다.
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) 코사인 덧셈법칙 입니다.
- 이제 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 덧셈을 활용하면 구할 수 있습니다.
- 위 그림 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 라고 정의 합니다.
- 전체 삼각형의 넓이(면적)
알고 있는 정보를 바탕으로 sin(theta1) * a 라고 하면 O에서 PT 선분에 직각인 높이를 알게 됩니다. 이를 h 라고 하겠습니다.
선분 PT 의 길이는 b 라고 주어 졌으니, 면적은 0.5 * h * b = 0.5 * sin(theta1) * a * b 입니다.
- 삼각형 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 로 정의해 놓습니다.
- 삼각형 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를 분배하지 않고 일단 그대로 두겠습니다.
- 계산을 원활히 하기 위해 공통적인 영역을 바꿔 보겠습니다.
정의한 내용중 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
- 전체 삼각형의 넓이는 부분삼각형의 넓이의 합과 같습니다.
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());
}