WebGL Tutorial
and more

三角函数

撰写时间:2024-04-22

修订时间:2024-05-10

本页面下各小节的JavaScript代码,均事先声明了以下通用的变量及函数:

这样,既保证了代码可以正常运行,也突出了各小节的核心代码。

笛卡尔坐标系的建立

通过调用translatescale方法,我们能将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类定义了ToRadianToDegree方法。

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的两个参数分别是yx,而不是xy

所返回的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

任意两点的射线的倾斜角

要求出一条任意两点的射线的倾斜角,在Mathatan2方法的参数中,可用终点减去起点。

let ptSrc = Point(10, 10); let ptDst = Point(50, 30); 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(50, 30); 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;

代入两点中任意一点的xy,即可求出已知两点射线中的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);

修改上面ptSrcptDst的坐标值,观察Console面板中kb的值的变化。

求直线上任意一点的坐标

再回看斜截式直线公式:

y = kx + b

kb均已求出,只剩下自变量yx了。这意味着在该线段中的任意一点,只要我们知道其xy中的任意一个数值,我们就能求出该点的坐标来。

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); }

参考资源

  1. HTML5 Canvas Element