三角函数
撰写时间:2024-04-22
修订时间:2024-08-09
本页面下各小节的JavaScript代码,均事先声明了以下通用的变量及函数:
这样,既保证了代码可以正常运行,也突出了各小节的核心代码。
笛卡尔坐标系的建立
通过调用translate及scale方法,我们能将Canvas 2D的坐标系转换为笛卡尔坐标系。
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
ctx.strokeStyle = 'gray';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, 100);
ctx.stroke();
三角函数
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let point = Point(100, 70);
drawAxis();
drawLine(Point(0, 0), point);
drawLine(Point(0, 0), Point(point.x, 0));
drawLine(point, Point(point.x, 0));
drawLabel('a', point.x + 20, point.y / 2);
drawLabel('b', point.x / 2, -20);
drawLabel('c', point.x / 2 - 5, point.y / 2 + 15);
drawSlopeAngle(point, 30, 'Θ');
对于∠Θ,设a为其对边,b为底边,c为斜边,则有:
sin⍬ = a / c // 正弦,Math.sin()
cos⍬ = b / c // 余弦,Math.cos()
tan⍬ = a / b // 正切,Math.tan()
cot⍬ = b / a // 余切
sec⍬ = c / b // 正割
csc⍬ = c / a // 余割
前面三个最为常用。
直角三角函数的意义在于,对于一个直角三角形,确定了两边及其对应角度的关系。在这3个自变量中,只要确定了其中的两个自变量的值,即可求出另一个自变量的值来。
而根据两边的值求出sin⍬, cos⍬, tan⍬的值后,可根据相应的反三角函数来求出相应的角度值。
Math.asin(⍬) // 反正弦
Math.acos(⍬) // 反余弦
Math.atan(⍬) // 反正切
结合上面两步,直角三角形的角度与对应两条边的关系也就确定下来了。
在圆内建立三角函数的图形
在Canvas 2D环境中,我们经常在圆内建立起三角函数的图形,斜边为圆的半径。
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let point = Point(100, 70);
let radius = Math.sqrt(point.x ** 2 + point.y ** 2);
drawAxis();
drawLine(Point(0, 0), point);
drawLine(Point(0, 0), Point(point.x, 0));
drawLine(point, Point(point.x, 0));
drawLabel('a', point.x + 10, point.y / 2);
drawLabel('b', point.x / 2, -20);
drawLabel('c', point.x / 2 - 5, point.y / 2 + 15);
drawSlopeAngle(point, 30, 'Θ');
drawCircle(Point(0, 0), radius, 'transparent', 'gray');
圆弧上的点坐标
给定一个圆的半径及其一条射线与X轴正轴的夹角这两个条件,即可确定该射线与圆弧的交点。
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let radius = 100;
let degree = 30;
let point = getPointOnCircle(radius, degree);
drawAxis();
drawCircle(Point(0, 0), radius, 'transparent', 'gray');
drawLine(Point(0, 0), Point(radius, 0));
drawLine(Point(0, 0), Point(point.x, point.y));
drawIntersectAngle(point, 20, degree);
drawLabel('r', point.x / 2 - 5, point.y / 2 + 15);
let labelPoint = getPointOnCircle(radius + 30, degree);
drawLabel('A (x, y)', labelPoint.x, labelPoint.y, 'yellow');
对于给定的条件:
let radius = 100;
let degree = 30;
函数getPointOnCircle可求出该射线与圆弧的交点的坐标:
function getPointOnCircle(radius, degree) {
let radian = Math.PI / 180 * degree;
let x = Math.cos(radian) * radius;
let y = Math.sin(radian) * radius;
return Point(x, y);
}
因为经常需要进行弧度与角度之间的换算,本站编写的MathUtils类定义了ToRadian及ToDegree方法。
export class MathUtils {
...
static ToRadian(degree) {
return Math.PI / 180 * degree;
}
static ToDegree(radian) {
return 180 / Math.PI * radian;
}
}
因此可将上面相应代码改为:
let radian = MathUtils.ToRadian(degree);
射线与角度的关系
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let radius = 100;
SECONDS_PER_CYCLE = 5;
requestAnimationFrame(initFirstFrame);
function doInAnimate(currAngle) {
let point = getPointOnCircle(radius, currAngle);
drawAxis();
drawCircle(Point(0, 0), radius, 'transparent');
drawLine(Point(0, 0), Point(radius, 0));
drawLine(Point(0, 0), Point(point.x, point.y));
drawIntersectAngle(point, 20, currAngle);
let labelPoint = getPointOnCircle(radius + 20, currAngle);
drawLabel('A (x, y)', labelPoint.x, labelPoint.y, 'yellow');
}
CJS通用代码中定义了以下与动画有关的函数:
let angle = 0;
let SECONDS_PER_CYCLE = 5;
let prevTimeStamp;
function initFirstFrame(timeStamp) {
prevTimeStamp = timeStamp;
requestAnimationFrame(animate);
}
function animate(timeStamp) {
ctx.clearRect(-canvas.clientWidth / 2, -canvas.clientHeight / 2, canvas.clientWidth, canvas.clientHeight);
let duration = timeStamp - prevTimeStamp;
let dpms = 360 / (SECONDS_PER_CYCLE * 1000);
angle += dpms * duration;
doInAnimate(angle);
prevTimeStamp = timeStamp;
requestAnimationFrame(animate);
}
代表转速的常量SECONDS_PER_CYCLE表示转一圈需要多少秒,变量dpms (degree per millisecond) 依此算出每毫秒转多少度。而在animate函数中,根据每次渲染时距离上次渲染所流逝的毫秒数duration来计算出角度每次的增量,并累加进angle中。然后,以其作为参数回调doInAnimate函数。这样,我们可集中在doInAnimate函数设置动画。
试着修改常量SECONDS_PER_CYCLE的值,观察运行效果。
这种算法,只与时间有关,而与具体计算机的主频无关,可确保在不同的计算机上其速度都完全一样。
我们看到,随着角度的增大,该条射线在笛卡尔坐标系上是逆时针旋转的。
斜率
倾斜角的概念
Math的静态方法atan2可求出从原点引出的一条射线相对于X轴正半轴的夹角。
let point = Point(100, 50);
let slopeRadian = Math.atan2(point.y, point.x);
let degree = 180 / Math.PI * slopeRadian;
需注意的是,atan2的两个参数分别是y及x,而不是x及y。
所返回的slopeRadian是表示射线倾斜度的一个角度,用弧度制表示,是这条射线相对于X轴正半轴的夹角。我们称之为倾斜角(slope angle)。上面将其换为角度制。其在笛卡尔坐标系中的示意图如下:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let point = Point(100, 50);
let slope = Math.atan2(point.y, point.x);
let degree = slope * 180 / Math.PI;
drawAxis();
drawLine(Point(0, 0), point);
drawLabel(`Point(${point.x}, ${point.y})`, point.x + 20, point.y + 20, 'yellow');
drawIntersectAngle(point, 20, degree);
即,在笛卡尔坐标系上,从原点到点(100, 50)的射线,其与X轴正半轴的夹角为26o。
在笛卡尔坐标系中,倾斜角的值及射线方向的规律如下表:
象限或轴向 | 倾斜角值域 | 射线朝向 |
X正轴 | 0 | 左到右 |
第一象限 | [1, 89] | 左下到右上 |
Y正轴 | 90 | 下到上 |
第二象限 | [91, 179] | 右下到左上 |
X负轴 | 180 | 右到左 |
第三象限 | [-91, -179] | 右上到左下 |
Y负轴 | -90 | 上到下 |
第四象限 | [-1, -89] | 左上到右下 |
根据倾斜角的值来判断射线的上下朝向:
倾斜角值域 | 射线朝向 |
[1, 179] | 从下到上 |
[-1, -179] | 从上到下 |
根据倾斜角的绝对值来判断射线的左右朝向:
倾斜角值域 | 射线朝向 |
[0, 89] | 从左到右 |
[91, 179] | 从右到左 |
任意两点的射线的倾斜角
要求出一条任意两点的射线的倾斜角,在Math的atan2方法的参数中,可用终点减去起点。
let ptSrc = Point(10, 10);
let ptDst = Point(80, 50);
let slopeRadian = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
完整代码:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let ptSrc = Point(10, 10);
let ptDst = Point(80, 50);
let slopeRadian = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let degree = MathUtils.ToDegree(slopeRadian);
console.log(degree);
drawAxis();
drawLine(ptSrc, ptDst);
斜截式直线公式中的斜率
斜截式直线公式为:
y = kx + b
其中,k为斜率(slope),系以数值的方式来表示直线的倾斜度,它是上面所提到的倾斜角的正切值。
k = Math.tan(slopeAngle);
倾斜角是表示直线倾斜的角度,斜率是倾斜角的正切值。记住这一点,不要混淆。
解斜截式直线公式方程
根据上节,对于任意两点所组成的直线,我们能求出其倾斜角,进而能求出其斜率来:
let ptSrc = Point(10, 10);
let ptDst = Point(50, 30);
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
则斜截式直线公式中的截距b为:
let ptSrc = Point(10, 10);
let ptDst = Point(50, 30);
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = y - k * x;
代入两点中任意一点的x及y,即可求出已知两点射线中的b。
let ptSrc = Point(10, 10);
let ptDst = Point(50, 30);
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = ptDst.y - k * ptDst.x;
绘图:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let ptSrc = Point(-30, -10);
let ptDst = Point(40, 50);
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = ptDst.y - k * ptDst.x;
console.log(k, b);
drawAxis();
drawLine(ptSrc, ptDst);
修改上面ptSrc及ptDst的坐标值,观察Console面板中k与b的值的变化。
求直线上任意一点的坐标
再回看斜截式直线公式:
y = kx + b
k与b均已求出,只剩下自变量y与x了。这意味着在该线段中的任意一点,只要我们知道其x或y中的任意一个数值,我们就能求出该点的坐标来。
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function getRandomPointOnLineSegment(ptSrc, ptDst) {
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = ptDst.y - k * ptDst.x;
let x = getRandomIntInclusive(Math.min(ptSrc.x, ptDst.x), Math.max(ptSrc.x, ptDst.x));
let y = k * x + b;
return Point(x, y);
}
效果如下:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let ptSrc = Point(-80, -10);
let ptDst = Point(40, 50);
requestAnimationFrame(initFirstFrame);
function doInAnimate(currAngle) {
drawAxis();
let point = getRandomPointOnLineSegment(ptSrc, ptDst);
drawCircle(point, 5);
drawLine(ptSrc, ptDst);
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function getRandomPointOnLineSegment(ptSrc, ptDst) {
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = ptDst.y - k * ptDst.x;
let x = getRandomIntInclusive(Math.min(ptSrc.x, ptDst.x), Math.max(ptSrc.x, ptDst.x));
let y = k * x + b;
return Point(x, y);
}
上面,我们先随机取出线段中任意一点的x值,然后再求出该点对应的y值。
函数getRandomPointOnLineSegment的意义:已知任意两点坐标,我们能求出这两点所组成的线段中任意一点的坐标。
当然,如果不要求限定于线段中的点,则可以扩展为取出直线上任意一点的坐标:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let ptSrc = Point(-80, -10);
let ptDst = Point(40, 50);
requestAnimationFrame(initFirstFrame);
function doInAnimate(currAngle) {
drawAxis();
let point = getRandomPointOnLine(ptSrc, ptDst);
drawCircle(point, 5);
drawLine(ptSrc, ptDst);
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function getRandomPointOnLine(ptSrc, ptDst) {
let slopeAngle = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
let k = Math.tan(slopeAngle);
let b = ptDst.y - k * ptDst.x;
let x = getRandomIntInclusive(-canvas.clientWidth / 2, canvas.clientWidth / 2);
let y = k * x + b;
return Point(x, y);
}
函数getRandomPointOnLine的意义:已知任意两点坐标,我们能求出这两点所组成的直线上任意一点的坐标。
求线段特定偏移位置的点坐标
如果要根据线段的偏移位置求出点的坐标,则更为简单:
function getPointOnLineSegmentByOffset(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
效果:
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let ptSrc = Point(-80, -10);
let ptDst = Point(40, 50);
requestAnimationFrame(initFirstFrame);
function doInAnimate(currAngle) {
currAngle = currAngle % 360;
let offset = currAngle / 360;
drawLine(ptSrc, ptDst);
let point = getPointOnLineSegmentByOffset(ptSrc, ptDst, offset);
drawCircle(point, 5);
}
function getPointOnLineSegmentByOffset(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
三角箭头的绘制
ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2);
ctx.scale(1, -1);
let pt1 = Point(-50, 30);
let pt2 = Point(-80, 85);
drawAxis();
drawLineWithArrow(pt1, pt2, 25, 15);
function drawLineWithArrow(ptSrc, ptDst, degree = 25, length = 15) {
ctx.save();
drawLine(ptSrc, ptDst);
ctx.translate(ptDst.x, ptDst.y);
let slopeRadian = Math.atan2(ptDst.y - ptSrc.y, ptDst.x - ptSrc.x);
ctx.rotate(slopeRadian);
let x = Math.cos(MathUtils.ToRadian(180 - degree)) * length;
let y = Math.sin(MathUtils.ToRadian(180 - degree)) * length;
drawLine(Point(0, 0), Point(x, y));
drawLine(Point(0, 0), Point(x, -y));
ctx.restore();
}
实现三角箭头的重点是箭头的旋转问题。这里先将坐标系平移至终点的位置,再根据线段的倾斜角进行旋转,从而得出正确方向的箭头。这里仍需注意一个问题,我们所指定的夹角与正常的三角函数的夹角左右颠倒,因此需用180o来减去所指定的夹角。
由于经常需要绘制三角箭头,因此在CtxDrawingUtils类中实现了此方法,CanvasEditor中可以直接调用:
let pt1 = Point(50, 50);
let pt2 = Point(100, 100);
ctxUtils.drawLineWithArrow(pt1, pt2, 25, 15, 'deepskyblue');