如何在JavaScript中绘制tan(x)、ln(x)等含渐近行为的函数?
处理含渐近线数学函数的JavaScript绘图优化
问题描述
我正在开发一个项目,需要在JavaScript中绘制数学函数图像,尤其是具有渐近行为或在某些点趋近于无穷的函数,例如tan(x)、ln(x)和log(x)。我难以准确呈现这些函数,尤其是在它们趋近于无穷的点附近(比如tan(x)的垂直渐近线附近)。
绘制y = x+2这类普通函数没有问题,但绘制y = tan(x)时出现异常——图像会在渐近线附近错误地连接两端。计算值是正确的,问题出在绘制逻辑上。
以下是我的函数绘制代码:
export function drawFunction(latexStr) { let isTan; // Draw function console.log(canvas.height); console.log(canvas.width); let prevY; // Stores the previous Y value let startY = true; // To know when a new line is needed ctx.beginPath(); ctx.strokeStyle = "blue"; ctx.lineWidth = 2; // Draw the function in small steps for (let x = -canvas.width / 2; x <= canvas.width / 2; x += stepSize) { let y = parser(latexStr, x)[0]; // f(x) isTan = parser(latexStr, x)[1]; console.log(isTan); // Skip drawing the graph if it's infinite or too large for the canvas if (Math.abs(y) > canvas.height / scaleFactor && !isTan) { startY = true; y = (y < 0 ? -1 : 1) * (canvas.height / scaleFactor - 1); } const adjustedX = canvas.width / 2 + x * scaleFactor; // Calculation of the small X line const adjustedY = canvas.height / 2 - y * scaleFactor; // Calculation of the small Y line /* if (Math.round(x * 100) / 100 === stepSize) ctx.moveTo(adjustedX, adjustedY); */ // Draw or move to position if (startY) { ctx.moveTo(adjustedX, adjustedY); startY = false; } else { if (prevY !== undefined && Math.abs(y - prevY) > 1 * scaleFactor) { if (isTan) { ctx.stroke(); // Draw the current line ctx.beginPath(); // Start a new line ctx.moveTo(adjustedX, y < 0 ? canvas.height : 0); // Move to the edge of the canvas } else { ctx.moveTo(adjustedX, adjustedY); // Move to the new starting position } } else { ctx.lineTo(adjustedX, adjustedY); } } prevY = y; //console.log(Math.round(x * 100) / 100); } ctx.stroke(); // Draw the function }
优化方案
1. 通用化渐近线检测,摆脱特定函数依赖
当前代码通过isTan标记单独处理正切函数,无法适配其他含渐近线的函数。改为通过函数值本身判断:
- 当
y为Infinity、-Infinity,或超出画布可显示的Y范围时,判定为渐近线区域,需要断开路径。 - 删除原有的
isTan分支逻辑,统一用无穷值/越界检测处理所有函数。
2. 正确处理渐近线前后的路径断开
函数在渐近线附近会出现正负无穷的跳变,此时不能直接用lineTo连接前后点,正确流程是:
- 若之前有有效绘制点,先将当前段绘制到画布边缘(对应无穷方向);
- 结束当前路径,重新开启新路径;
- 在渐近线另一侧,从画布边缘开始绘制下一段曲线。
修改后的核心逻辑示例:
export function drawFunction(latexStr) { // Draw function const maxY = canvas.height / scaleFactor; let prevY; let startY = true; ctx.beginPath(); ctx.strokeStyle = "blue"; ctx.lineWidth = 2; for (let x = -canvas.width / 2; x <= canvas.width / 2; x += stepSize) { const [y] = parser(latexStr, x); const isInfinite = !isFinite(y) || Math.abs(y) > maxY; const adjustedX = canvas.width / 2 + x * scaleFactor; if (isInfinite) { // 绘制当前段到画布边缘并断开路径 if (!startY) { const edgeY = prevY > 0 ? canvas.height / 2 - maxY * scaleFactor : canvas.height / 2 + maxY * scaleFactor; ctx.lineTo(adjustedX, edgeY); ctx.stroke(); ctx.beginPath(); } startY = true; prevY = undefined; } else { const adjustedY = canvas.height / 2 - y * scaleFactor; if (startY) { ctx.moveTo(adjustedX, adjustedY); startY = false; } else { // 检测是否是跨渐近线的正负跳变 const isJump = prevY !== undefined && Math.sign(y) !== Math.sign(prevY) && Math.abs(y - prevY) > maxY * 0.5; if (isJump) { // 绘制前一段到边缘,新开路径 const prevEdgeY = prevY > 0 ? canvas.height / 2 - maxY * scaleFactor : canvas.height / 2 + maxY * scaleFactor; ctx.lineTo(adjustedX, prevEdgeY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(adjustedX, adjustedY); } else { ctx.lineTo(adjustedX, adjustedY); } } prevY = y; } } // 绘制最后一段未完成的路径 ctx.stroke(); }
3. 优化采样策略
- 提前规避渐近线:对于
tan(x)这类有周期性渐近线的函数,可提前计算渐近线位置(x = π/2 + kπ,k为整数),采样时跳过渐近线附近的极小区间,避免无效计算。 - 动态步长调整:在函数变化平缓区域用大步长提升性能,在渐近线附近用小步长保证绘制精度。
4. 清理冗余代码
移除原代码中依赖isTan的特殊逻辑,让代码更简洁,同时支持ln(x)、1/x等更多含渐近线的函数。
内容的提问来源于stack exchange,提问作者Simone
相关产品推荐
相关产品推荐

