You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HTML5 Canvas三种椭圆/圆弧绘制方法实现问询:arcTo与贝塞尔曲线

实现任意椭圆/圆弧的三种绘制方法(arcTo、三次贝塞尔、二次贝塞尔)

我明白你想搞定从起点到终点绘制任意参数椭圆/圆弧的需求——不管是整段还是分段、外凸还是内凹,还要支持任意角度和半轴长度。下面我把三个函数的实现逻辑拆解清楚,再把代码完善好,你可以直接用~

一、借助坐标变换用arcTo绘制椭圆弧

原生Canvas的ctx.arcTo()只能绘制圆弧,要画椭圆弧的核心思路是先把椭圆通过坐标变换转换成圆弧,绘制后再还原坐标系。具体步骤:

  1. 计算椭圆的中心、起点/终点对应的椭圆参数角
  2. 对画布进行平移、旋转、缩放,把椭圆映射成单位圆
  3. 用圆弧绘制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°时误差可忽略),核心逻辑是:

  1. 根据approximationLevel将椭圆弧拆分成若干小段
  2. 用椭圆参数方程推导每段弧对应的三次贝塞尔控制点
  3. 依次调用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();
}

三、用二次贝塞尔曲线近似椭圆弧

二次贝塞尔是抛物线,无法完美匹配椭圆弧,但可以通过多段分段逼近达到足够精度。核心逻辑:

  1. 将椭圆弧拆分成更多小段(approximationLevel越高精度越好)
  2. 通过椭圆上三点拟合每段对应的二次贝塞尔控制点
  3. 依次调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:49:13