HTML5 Canvas三种椭圆/圆弧绘制方法实现问询:arcTo与贝塞尔曲线
实现任意椭圆/圆弧的三种绘制方法(arcTo、三次贝塞尔、二次贝塞尔)
我明白你想搞定从起点到终点绘制任意参数椭圆/圆弧的需求——不管是整段还是分段、外凸还是内凹,还要支持任意角度和半轴长度。下面我把三个函数的实现逻辑拆解清楚,再把代码完善好,你可以直接用~
一、借助坐标变换用arcTo绘制椭圆弧
原生Canvas的ctx.arcTo()只能绘制圆弧,要画椭圆弧的核心思路是先把椭圆通过坐标变换转换成圆弧,绘制后再还原坐标系。具体步骤:
- 计算椭圆的中心、起点/终点对应的椭圆参数角
- 对画布进行平移、旋转、缩放,把椭圆映射成单位圆
- 用圆弧绘制API完成绘制后,恢复原始坐标系
完善后的arcTo函数
function arcTo(ctx, xi, yi, r1, r2, xf, yf, bulgeIn = false, rotationAngle = 0) { ctx.save(); ctx.beginPath(); ctx.moveTo(xi, yi); // 简化计算椭圆中心(若需精确解可扩展为椭圆方程求解) const cx = (xi + xf) / 2; const cy = (yi + yf) / 2; // 坐标变换:将椭圆转为单位圆 ctx.translate(cx, cy); ctx.rotate(rotationAngle); ctx.scale(r1, r2); // 计算变换后的起点/终点(单位圆上的点) const transformedXi = (xi - cx) / r1 * Math.cos(-rotationAngle) - (yi - cy) / r2 * Math.sin(-rotationAngle); const transformedYi = (xi - cx) / r1 * Math.sin(-rotationAngle) + (yi - cy) / r2 * Math.cos(-rotationAngle); const transformedXf = (xf - cx) / r1 * Math.cos(-rotationAngle) - (yf - cy) / r2 * Math.sin(-rotationAngle); const transformedYf = (xf - cx) / r1 * Math.sin(-rotationAngle) + (yf - cy) / r2 * Math.cos(-rotationAngle); // 计算圆弧的起始/结束角度 const startAngle = Math.atan2(transformedYi, transformedXi); const endAngle = Math.atan2(transformedYf, transformedXf); // 控制弧的凸向:bulgeIn为true时绘制内凹长弧 let anticlockwise = bulgeIn; let angleDiff = endAngle - startAngle; if ((angleDiff > 0 && anticlockwise) || (angleDiff < 0 && !anticlockwise)) { angleDiff += anticlockwise ? -2 * Math.PI : 2 * Math.PI; } // 绘制单位圆弧后恢复坐标系 ctx.arc(0, 0, 1, startAngle, endAngle, anticlockwise); ctx.restore(); ctx.stroke(); }
二、用三次贝塞尔曲线近似椭圆弧
三次贝塞尔曲线可以非常接近椭圆弧(单段角度≤90°时误差可忽略),核心逻辑是:
- 根据
approximationLevel将椭圆弧拆分成若干小段 - 用椭圆参数方程推导每段弧对应的三次贝塞尔控制点
- 依次调用
ctx.bezierCurveTo()拼接完整弧
完善后的cubicTo函数
function cubicTo(ctx, xi, yi, r1, r2, xf, yf, bulgeIn = false, approximationLevel = 4, rotationAngle = 0) { ctx.save(); ctx.beginPath(); ctx.moveTo(xi, yi); // 计算椭圆中心 const cx = (xi + xf) / 2; const cy = (yi + yf) / 2; // 获取点对应的椭圆参数角 function getEllipseAngle(x, y) { const dx = x - cx; const dy = y - cy; const rx = dx * Math.cos(-rotationAngle) + dy * Math.sin(-rotationAngle); const ry = -dx * Math.sin(-rotationAngle) + dy * Math.cos(-rotationAngle); return Math.atan2(ry / r2, rx / r1); } let startAngle = getEllipseAngle(xi, yi); let endAngle = getEllipseAngle(xf, yf); // 调整角度差,确定分段数 let angleDiff = endAngle - startAngle; if (bulgeIn) { angleDiff = angleDiff > 0 ? angleDiff - 2 * Math.PI : angleDiff + 2 * Math.PI; } const segmentCount = Math.max(Math.abs(Math.round(angleDiff / (Math.PI / approximationLevel))), 1); const segmentAngle = angleDiff / segmentCount; // 三次贝塞尔近似椭圆的系数(单位圆适配) const k = 4 / 3 * Math.tan(segmentAngle / 4); // 逐段绘制贝塞尔曲线 for (let i = 0; i < segmentCount; i++) { const currentAngle = startAngle + i * segmentAngle; const nextAngle = currentAngle + segmentAngle; // 计算当前点、下一点坐标 const x0 = cx + r1 * Math.cos(currentAngle) * Math.cos(rotationAngle) - r2 * Math.sin(currentAngle) * Math.sin(rotationAngle); const y0 = cy + r1 * Math.cos(currentAngle) * Math.sin(rotationAngle) + r2 * Math.sin(currentAngle) * Math.cos(rotationAngle); const x3 = cx + r1 * Math.cos(nextAngle) * Math.cos(rotationAngle) - r2 * Math.sin(nextAngle) * Math.sin(rotationAngle); const y3 = cy + r1 * Math.cos(nextAngle) * Math.sin(rotationAngle) + r2 * Math.sin(nextAngle) * Math.cos(rotationAngle); // 计算两个控制点 const x1 = x0 - k * r1 * Math.sin(currentAngle) * Math.cos(rotationAngle) - k * r2 * Math.cos(currentAngle) * Math.sin(rotationAngle); const y1 = y0 + k * r1 * Math.sin(currentAngle) * Math.sin(rotationAngle) - k * r2 * Math.cos(currentAngle) * Math.cos(rotationAngle); const x2 = x3 + k * r1 * Math.sin(nextAngle) * Math.cos(rotationAngle) + k * r2 * Math.cos(nextAngle) * Math.sin(rotationAngle); const y2 = y3 - k * r1 * Math.sin(nextAngle) * Math.sin(rotationAngle) + k * r2 * Math.cos(nextAngle) * Math.cos(rotationAngle); ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); } ctx.stroke(); ctx.restore(); }
三、用二次贝塞尔曲线近似椭圆弧
二次贝塞尔是抛物线,无法完美匹配椭圆弧,但可以通过多段分段逼近达到足够精度。核心逻辑:
- 将椭圆弧拆分成更多小段(
approximationLevel越高精度越好) - 通过椭圆上三点拟合每段对应的二次贝塞尔控制点
- 依次调用
ctx.quadraticCurveTo()拼接完整弧
完善后的quadTo函数
function quadTo(ctx, xi, yi, r1, r2, xf, yf, bulgeIn = false, approximationLevel = 6, rotationAngle = 0) { ctx.save(); ctx.beginPath(); ctx.moveTo(xi, yi); // 计算椭圆中心 const cx = (xi + xf) / 2; const cy = (yi + yf) / 2; // 获取点对应的椭圆参数角 function getEllipseAngle(x, y) { const dx = x - cx; const dy = y - cy; const rx = dx * Math.cos(-rotationAngle) + dy * Math.sin(-rotationAngle); const ry = -dx * Math.sin(-rotationAngle) + dy * Math.cos(-rotationAngle); return Math.atan2(ry / r2, rx / r1); } let startAngle = getEllipseAngle(xi, yi); let endAngle = getEllipseAngle(xf, yf); // 调整角度差,确定分段数 let angleDiff = endAngle - startAngle; if (bulgeIn) { angleDiff = angleDiff > 0 ? angleDiff - 2 * Math.PI : angleDiff + 2 * Math.PI; } const segmentCount = Math.max(Math.abs(Math.round(angleDiff / (Math.PI / approximationLevel))), 1); const segmentAngle = angleDiff / segmentCount; // 逐段绘制二次贝塞尔曲线 for (let i = 0; i < segmentCount; i++) { const currentAngle = startAngle + i * segmentAngle; const midAngle = currentAngle + segmentAngle / 2; const nextAngle = currentAngle + segmentAngle; // 计算当前点、中点、下一点坐标 const x0 = cx + r1 * Math.cos(currentAngle) * Math.cos(rotationAngle) - r2 * Math.sin(currentAngle) * Math.sin(rotationAngle); const y0 = cy + r1 * Math.cos(currentAngle) * Math.sin(rotationAngle) + r2 * Math.sin(currentAngle) * Math.cos(rotationAngle); const xMid = cx + r1 * Math.cos(midAngle) * Math.cos(rotationAngle) - r2 * Math.sin(midAngle) * Math.sin(rotationAngle); const yMid = cy + r1 * Math.cos(midAngle) * Math.sin(rotationAngle) + r2 * Math.sin(midAngle) * Math.cos(rotationAngle); const x2 = cx + r1 * Math.cos(nextAngle) * Math.cos(rotationAngle) - r2 * Math.sin(nextAngle) * Math.sin(rotationAngle); const y2 = cy + r1 * Math.cos(nextAngle) * Math.sin(rotationAngle) + r2 * Math.sin(nextAngle) * Math.cos(rotationAngle); // 通过三点拟合二次贝塞尔控制点 const x1 = (4 * xMid - x0 - x2) / 2; const y1 = (4 * yMid - y0 - y2) / 2; ctx.quadraticCurveTo(x1, y1, x2, y2); } ctx.stroke(); ctx.restore(); }
使用示例
你可以直接调用这三个函数测试效果:
var canvas = document.querySelector('canvas') var ctx = canvas.getContext('2d') canvas.width = 400; canvas.height = 400; // 红色:arcTo方法绘制的椭圆弧 ctx.strokeStyle = '#f00'; arcTo(ctx, 50, 50, 80, 50, 250, 50, false, Math.PI/6); // 绿色:三次贝塞尔方法绘制的椭圆弧 ctx.strokeStyle = '#0f0'; cubicTo(ctx, 50, 150, 80, 50, 250, 150, false, 4, Math.PI/6); // 蓝色:二次贝塞尔方法绘制的椭圆弧 ctx.strokeStyle = '#00f'; quadTo(ctx, 50, 250, 80, 50, 250, 250, false, 6, Math.PI/6);
内容的提问来源于stack exchange,提问作者user10869858
相关产品推荐
相关产品推荐

