三角函数sin、cos、tan、sinh、cosh、tanh
js 的Math库,自带了许多常见函数方法,其中关于 三角函数的求解还是很齐全的,于是想到,利用canvas 绘制一个比较准确的 sin、cos、tan、sinh、cosh、tanh图像。
canvas绘制坐标轴
坐标轴由两条直线构成,直接将坐标定在中心点就好,这样容易绘制。同时为坐标的箭头定义边长。
//xy轴
function drawXY() {
var ox = Math.floor(width / 2) + fix1px;//1px显示变粗修复
var oy = Math.floor(height / 2) + fix1px;//1px显示变粗修复
//x轴
cxt.moveTo(0, oy);
cxt.lineTo(width, oy);
//y轴
cxt.moveTo(ox, height);
cxt.lineTo(ox, 0);
// cxt.stroke();
//x箭头
// cxt.beginPath();
cxt.moveTo(width - arrowLin -cxt.lineWidth/2, oy - arrowDui);
cxt.lineTo(width, oy);
cxt.lineTo(width - arrowLin -cxt.lineWidth/2, oy + arrowDui);
// cxt.closePath();
// cxt.stroke();
//y箭头
// cxt.beginPath();
cxt.moveTo(ox - arrowDui, arrowLin+cxt.lineWidth/2);
cxt.lineTo(ox, 0);
cxt.lineTo(ox + arrowDui, arrowLin+cxt.lineWidth/2);
// cxt.closePath();
cxt.stroke();
}
绘制坐标轴上的尺度和文字
思路:将坐标轴线的长度平均分割,分别定义好x轴和y轴每个尺度的对应值。
这其中,sin,cos和tan的图一般都是以π作为单位,且π/2也做标注,故画需要对每个单元格进行更细小的划分,按更小的单位进行循环绘制。类似直尺上的毫米,一毫米一毫米绘制,每十毫米满一个单元时,其标尺标记更长一些,以示区分。
为灵活展示每个单元的更细小划分情况,应允许传入一个单元的拆分数,用来展示更小单位下的尺度。如一厘米=10毫米,即拆分为10份。
对sin、cos、tan 来说,需要展示标注文字和标尺:1π/2,3π/2,则每个单元应拆分为2份。
但对sinh、cosh、tanh 来说,x、y轴上都不需要展示1/2,1/3的标注文字,也不需要展示特别细小的尺寸。份数可设置为1。
其实,这只是因为过小的尺寸不好标注文字。限制每多少个小单位标注文字即可。
//单元格
function drawUnitXY() {
//x轴分割
var x = 0;
//整除
// var xExact = deConfig.cellXs % 2 === 0;
var xText = '';
cxt.beginPath();
//x轴
var xCount = Math.floor((deConfig.cellXs * deConfig.cellXMins) / 2);
// xExact ? (xCount = xCount - 1) : xCount;
cxt.font = deConfig.fontSize + 'px sinsum';
// 起始和结束不绘制
for (var i = 1; i <= xCount; i++) {
//负数轴
x = Math.floor(width / 2 - i * cellXL / deConfig.cellXMins);
if (x > 0) {
x = x + fix1px;
//当i%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
if (i % deConfig.cellXMins === 0) {
cxt.moveTo(x, height / 2 - deConfig.cellLH);
} else {
cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
}
cxt.lineTo(x, height / 2);
xText = -i / deConfig.cellXMins + "" + deConfig.cellXUnit;
if (i % (deConfig.spaceXMText + 1) === 0) {
cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
}
}
//正数轴
x = Math.floor(width / 2 + i * cellXL / deConfig.cellXMins);
if (x < width) {
x = x + fix1px;//1px显示粗细修复
if (i % deConfig.cellXMins === 0) {
cxt.moveTo(x, height / 2 - deConfig.cellLH);
} else {
cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
}
cxt.lineTo(x, height / 2 - 0.5);
xText = i / deConfig.cellXMins + "" + deConfig.cellXUnit;
if (i % (deConfig.spaceXMText + 1) === 0) {
cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
}
}
}
//y轴分割
var y = 0;
// var yExact = deConfig.cellYs % 2 === 0;
var yCount = Math.floor(deConfig.cellYs * deConfig.cellYMins / 2);
// yExact ? (yCount = yCount - 1) : yCount;
//整除
// 起始和结束不绘制
for (var j = 1; j <= yCount; j++) {
//负轴
y = Math.floor(height / 2 + j * cellYL / deConfig.cellYMins);
console.log(deConfig.fn, y, yCount, '下');
if (y < height) {
y = y + fix1px;
//当j%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
if (j % deConfig.cellYMins === 0) {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
} else {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
}
cxt.lineTo(width / 2, y)
if (j % (deConfig.spaceYMText + 1) === 0) {
cxt.fillText(-j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
}
}
//正轴
y = Math.floor(height / 2 - j * cellYL / deConfig.cellYMins);
console.log(deConfig.fn, y, yCount, '上');
if (y > 0) {
y = y + fix1px;
if (j % deConfig.cellYMins === 0) {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
} else {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
}
cxt.lineTo(width / 2, y)
if (j % (deConfig.spaceYMText + 1) === 0) {
cxt.fillText(j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
}
}
}
cxt.closePath();
cxt.stroke();
}
此处采取坐标原点向正负方向绘制,这样绘制逻辑清晰一些,但代码量会增加。另一种方式是从左往右,或从右往左绘制,这样需要计算起始绘制位置等值。
绘制函数变化曲线
只需要将画布x轴上每一个像素点代表的x的值,带入函数,求出函数y的值,并转化为画布坐标轴的对应的值。由此求出函数变化曲线在画布上的每一个点坐标,连接起来即可。因此,画布有多宽,就循环多少次,求坐标点,以使曲线平滑。
//函数曲线
function drawFnLine() {
var startXV = -width / 2 * oneXV;//最左方x的值
var startYV = height / 2 * oneYV;//最上方y的值
var yv = 0;
//为每个x像素点计算y值,并连接
var y = 0;
cxt.strokeStyle = deConfig.lineColor;
cxt.beginPath();
for (var x = 0; x <= width; x++) {
eval("yv = Math." + deConfig.fn + "(startXV + oneXV * x);")
y = (startYV - yv) / oneYV;
if (Math.abs(x) > width || Math.abs(y) > height || x <= 0 || x >= width) {
cxt.moveTo(x + fix1px, y);
continue;
}
cxt.lineTo(x + fix1px, y);
}
cxt.closePath();
cxt.stroke();
}
绘制结果和注意事项
h5 canvas绘制一像素曲线时,可能导致2px曲线的结果。
1px变2px的原因
这是由于绘制时,将画笔中线对齐坐标,必然有一半线宽在坐标之外,这一半的线宽,如果不是整数,会被自动加满为整数。(因为1像素就是最小单位了,0.x的像素不能显示)
- 当线宽为奇数,绘制坐标是整数时,那么无论如何,线条的矩形区域都坐标都多出0.5像素,于是补满,整体多出了1像素。
- 当线宽为偶数,绘制坐标是整数时,那么无论如何,画笔中线总是对齐坐标的,线条宽度不会产生偏差。
- 当线宽有小数,绘制坐标是整数时,那么无论如何,线条的矩形区域都坐标都存在小数,于是补满。整体也会多出1像素。
如:
当画笔线宽为1px,那么绘制直线L:(0,0),(100,0)时,画笔对齐 y 坐标 = 0。由于线宽的存在,实际绘制线条有一半线宽在y的负半轴。导致不可见。
那么线条矩形区域为:
Rect(left:0,top:-0.5,right:100;bottom:0.5)
0.5像素不可见,加满1像素,实际线条区域变为:
Rect(left:0,top:-1,right:100;bottom:1)
就变2px那么大了。
解决这个问题,我们只需要将线条的y坐标偏移0.5就行:
L:(0,0.5),(100,0.5)
当画笔中线对齐y坐标=0.5,时,线条矩形区域没有多余小数:
Rect(left:0,top:0,right:100,1)
这样,自然就是真的1px线条了。
综上,线宽为偶数不会产生误差,而线宽为小数,本来就无法准确绘制,不用处理了。
所以绘制时,我们只需要修正线宽为奇数的情况就行。甚至,只修复线宽1px的情况就可以,因为当线宽很大时,肉眼感知不到啊。
var fix1px = (cxt.lineWidth % 2 === 0) ?0: 0.5;
纵向线条,x坐标偏移:
x = x+fix1px ;
横向线条,y坐标偏移:
y = y+fix1px ;
1px线条变2px线条图示:
`
结果预览
如图:
完整代码:
<html lang="en">
<head>
<meta charset="UTF-8">
<!--<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;">-->
<title>三角函数图</title>
</head>
<body>
<canvas id="sinCanvas" height="300px" width="300px"></canvas>
<canvas id="cosCanvas" height="300px" width="300px"></canvas>
<canvas id="tanCanvas" height="300px" width="300px"></canvas>
<p></p>
<canvas id="sinhCanvas" height="300px" width="300px"></canvas>
<canvas id="coshCanvas" height="300px" width="300px"></canvas>
<canvas id="tanhCanvas" height="300px" width="300px"></canvas>
<script>
//线宽为奇数不能被整除,导致绘制坐标为整数时,上下偏移出0.5像素。又由于0.5像素不能被显示,
// 自动补足为1像素。于是整体像素+1。
// 具体如
//当绘制直线L:0,0),(100,0)时,画笔中线y坐标=0。由于线宽的存在,
//实际绘制线条有一半线宽在y的负半轴。导致不可见。
//如若绘制时,画笔中线坐标y为1,线宽也为1,那么线条区域Rect(left:0,top:-0.5,right:100;bottom:0.5)
//由于0.5像素不足以被显示,实际渲染时,上下各加0.5,于是线条变成2px:
// Rect(left:0,top:-1,right:100;bottom:1)
//当线宽为2时,则不存在这个现象。
//解决办法:
// var fix1px = (cxt.lineWidth % 2 === 0) ?0: 0.5;
//线宽不为整数,不用管。
//纵向线条,x坐标偏移
// x = x+fix1px ;
//横向线条,y坐标偏移
// y = y+ fix1px ;
var angularHelper = function (canvas, config) {
config = config ? config : {};
var deConfig = {
fn: config.fn ? config.fn : 'tan',
fontSize: 12,
// desColor:config.desColor?config.desColor:"",
lineColor: config.lineColor ? config.lineColor : "#7389B9",//曲线颜色
arrowL: config.arrowL ? config.arrowL : 4,//箭头线长
arrowArg: Math.PI / 3,//箭头夹角
cellXs: config.cellXs ? config.cellXs : 8,//x轴分割数
cellYs: config.cellYs ? config.cellYs : 6,//y轴分割数
cellLH: config.cellLH ? config.cellLH : 6,//分割线高度
cellXV: config.cellXV ? config.cellXV : Math.PI,//每个x单元的值(弧度)
cellYV: config.cellYV ? config.cellYV : 1,//每个y单元的值
cellXUnit: (config.cellXUnit || config.cellXUnit === '') ? config.cellXUnit : 'π',//单位
cellYUnit: (config.cellYUnit || config.cellYUnit === '') ? config.cellYUnit : '',//单位
cellXMins: config.cellXMins ? config.cellXMins : 1,//x轴一单位尺度拆分为更小份数
cellYMins: config.cellYMins ? config.cellYMins : 1,//y轴一单位尺度拆分为更小份数,
spaceXMText: config.spaceXMText ? config.spaceXMText : 0,//x轴每间隔多少个更小单位,标注文字
spaceYMText: config.spaceYMText ? config.spaceYMText : 0,//y轴每间隔多少个更小单位,标注文字
}
var cxt = canvas.getContext('2d');
cxt.lineWidth = 1;
var width = canvas.width;
var height = canvas.height;
var arrowL = deConfig.arrowL;
//箭头三角形中线长
var arrowLin = arrowL * Math.cos(deConfig.arrowArg / 2);
//箭头三角形底边/2
var arrowDui = arrowL * Math.sin(deConfig.arrowArg / 2);
//每份x对应的像素长度
var cellXL = width / deConfig.cellXs;
var cellYL = height / deConfig.cellYs;
//y=sin(x)
var oneXV = deConfig.cellXV / cellXL;//一个x像素点对应的x值
var oneYV = deConfig.cellYV / cellYL;//一个x像素点对应的y值
var fix1px = (cxt.lineWidth % 2 === 0) ? 0 : 0.5;
function draw() {
drawDes();
drawXY();
drawUnitXY();
drawFnLine();
}
//描述
function drawDes() {
cxt.font = deConfig.fontSize + 'px sinsum';
var fw = cxt.measureText('x').width;
cxt.fillText("y = " + deConfig.fn + '(x) x∈R', deConfig.fontSize, deConfig.fontSize);
cxt.fillText('x', width - fw, height / 2 + deConfig.fontSize)
cxt.fillText('y', width / 2 + fw, fw)
// cxt.stroke();
}
//xy轴
function drawXY() {
var ox = Math.floor(width / 2) + fix1px;//1px显示变粗修复
var oy = Math.floor(height / 2) + fix1px;//1px显示变粗修复
//x轴
cxt.moveTo(0, oy);
cxt.lineTo(width, oy);
//y轴
cxt.moveTo(ox, height);
cxt.lineTo(ox, 0);
// cxt.stroke();
//x箭头
// cxt.beginPath();
cxt.moveTo(width - arrowLin -cxt.lineWidth/2, oy - arrowDui);
cxt.lineTo(width, oy);
cxt.lineTo(width - arrowLin -cxt.lineWidth/2, oy + arrowDui);
// cxt.closePath();
// cxt.stroke();
//y箭头
// cxt.beginPath();
cxt.moveTo(ox - arrowDui, arrowLin+cxt.lineWidth/2);
cxt.lineTo(ox, 0);
cxt.lineTo(ox + arrowDui, arrowLin+cxt.lineWidth/2);
// cxt.closePath();
cxt.stroke();
}
//单元格
function drawUnitXY() {
//x轴分割
var x = 0;
//整除
// var xExact = deConfig.cellXs % 2 === 0;
var xText = '';
cxt.beginPath();
//x轴
var xCount = Math.floor((deConfig.cellXs * deConfig.cellXMins) / 2);
// xExact ? (xCount = xCount - 1) : xCount;
cxt.font = deConfig.fontSize + 'px sinsum';
// 起始和结束不绘制
for (var i = 1; i <= xCount; i++) {
//负数轴
x = Math.floor(width / 2 - i * cellXL / deConfig.cellXMins);
if (x > 0) {
x = x + fix1px;
//当i%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
if (i % deConfig.cellXMins === 0) {
cxt.moveTo(x, height / 2 - deConfig.cellLH);
} else {
cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
}
cxt.lineTo(x, height / 2);
xText = -i / deConfig.cellXMins + "" + deConfig.cellXUnit;
if (i % (deConfig.spaceXMText + 1) === 0) {
cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
}
}
//正数轴
x = Math.floor(width / 2 + i * cellXL / deConfig.cellXMins);
if (x < width) {
x = x + fix1px;//1px显示粗细修复
if (i % deConfig.cellXMins === 0) {
cxt.moveTo(x, height / 2 - deConfig.cellLH);
} else {
cxt.moveTo(x, height / 2 - deConfig.cellLH / 1.5);
}
cxt.lineTo(x, height / 2 - 0.5);
xText = i / deConfig.cellXMins + "" + deConfig.cellXUnit;
if (i % (deConfig.spaceXMText + 1) === 0) {
cxt.fillText(xText, x - cxt.measureText(xText).width / 2, height / 2 + deConfig.fontSize);
}
}
}
//y轴分割
var y = 0;
// var yExact = deConfig.cellYs % 2 === 0;
var yCount = Math.floor(deConfig.cellYs * deConfig.cellYMins / 2);
// yExact ? (yCount = yCount - 1) : yCount;
//整除
// 起始和结束不绘制
for (var j = 1; j <= yCount; j++) {
//负轴
y = Math.floor(height / 2 + j * cellYL / deConfig.cellYMins);
console.log(deConfig.fn, y, yCount, '下');
if (y < height) {
y = y + fix1px;
//当j%deConfig.cellXMins==0时,进位,画长线条,否则短线条,以做区分
if (j % deConfig.cellYMins === 0) {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
} else {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
}
cxt.lineTo(width / 2, y)
if (j % (deConfig.spaceYMText + 1) === 0) {
cxt.fillText(-j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
}
}
//正轴
y = Math.floor(height / 2 - j * cellYL / deConfig.cellYMins);
console.log(deConfig.fn, y, yCount, '上');
if (y > 0) {
y = y + fix1px;
if (j % deConfig.cellYMins === 0) {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH), y)
} else {
cxt.moveTo(Math.floor(width / 2 + deConfig.cellLH / 1.5), y)
}
cxt.lineTo(width / 2, y)
if (j % (deConfig.spaceYMText + 1) === 0) {
cxt.fillText(j / deConfig.cellYMins + "" + deConfig.cellYUnit, width / 2 + deConfig.fontSize, y + deConfig.fontSize / 2);
}
}
}
// cxt.closePath();
cxt.stroke();
}
//函数曲线
function drawFnLine() {
var startXV = -width / 2 * oneXV;//最左方x的值
var startYV = height / 2 * oneYV;//最上方y的值
var yv = 0;
//为每个x像素点计算y值,并连接
var y = 0;
cxt.strokeStyle = deConfig.lineColor;
cxt.beginPath();
for (var x = 0; x <= width; x++) {
eval("yv = Math." + deConfig.fn + "(startXV + oneXV * x);")
y = (startYV - yv) / oneYV;
if (Math.abs(x) > width || Math.abs(y) > height || x <= 0 || x >= width) {
cxt.moveTo(x + fix1px, y);
continue;
}
cxt.lineTo(x + fix1px, y);
}
// cxt.closePath();
cxt.stroke();
}
return {draw: draw};
}
var sinCanvas = document.getElementById('sinCanvas');
var cosCanvas = document.getElementById('cosCanvas');
var tanCanvas = document.getElementById('tanCanvas');
var sinhCanvas = document.getElementById('sinhCanvas');
var coshCanvas = document.getElementById('coshCanvas');
var tanhCanvas = document.getElementById('tanhCanvas');
angularHelper(sinCanvas, {fn: 'sin', cellXV: Math.PI, cellXs: 5, cellYs: 7, cellXMins: 2, cellYMins: 2}).draw();
angularHelper(cosCanvas, {fn: 'cos', cellXV: Math.PI, cellXs: 4, cellYs: 4, cellXMins: 2}).draw();
angularHelper(tanCanvas, {fn: 'tan', cellXV: Math.PI, cellXs: 5, cellYs: 4, cellXMins: 2, spaceXMText: 1}).draw();
angularHelper(sinhCanvas, {fn: 'sinh', cellXV: 1, cellXs: 8, cellXUnit: '', cellXMins: 10, spaceXMText: 4}).draw();
angularHelper(coshCanvas, {fn: 'cosh', cellXV: 1, cellXs: 7, cellXUnit: ''}).draw();
angularHelper(tanhCanvas, {fn: 'tanh', cellXV: 1, cellXs: 7, cellXUnit: ''}).draw();
</script>
</body>
</html>