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

如何选择控制点绘制更平滑的Bezier曲线?现有方案优化求助

问题描述

我用某算法给一系列坐标点添加控制点绘制贝塞尔曲线,但加载HTML代码后效果很差——曲线不够平滑,还存在尖点。我想实现像下图这样更凸的平滑曲线,该怎么选择控制点?

期望的平滑凸曲线

我的坐标点和代码如下:

坐标点

const xs = [942, 421, 75, 171, 479];
const ys = [406, 375, 222, 99, 38];

完整代码

JavaScript

const xs = [942, 421, 75, 171, 479];
const ys = [406, 375, 222, 99, 38];

function drawPoints(xs, ys, hostElement) {
    const len = Math.min(xs.length, ys.length);
    const r = 5;
    for (let i = 0; i < len; i++) {
        const ellipseElement = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
        ellipseElement.setAttribute('cx', xs[i]);
        ellipseElement.setAttribute('cy', ys[i]);
        ellipseElement.setAttribute('rx', r);
        ellipseElement.setAttribute('ry', r);
        ellipseElement.setAttribute('fill', "#5572c3");
        hostElement.appendChild(ellipseElement);
    }
}

function drawQuadBezierCurve(xs, ys, hostElement) {
    const curveSegments = buildCurveSegments(xs, ys);
    const xs_new = [];
    const ys_new = [];
    buildPolylinePoints(curveSegments, xs_new, ys_new);
    drawPolyline(xs_new, ys_new, hostElement);
}

function buildCurveSegments(xs, ys) {
    const result = [];
    const len = Math.min(xs.length, ys.length);
    for (let i = 0; i < len - 1; i++) {
        const x_cur = xs[i];
        const y_cur = ys[i];
        const x_next = xs[i + 1];
        const y_next = ys[i + 1];
        const x_mid = (x_cur + x_next) / 2;
        const y_mid = (y_cur + y_next) / 2;
        const cp_x1 = (x_mid + x_cur) / 2;
        const cp_x2 = (x_mid + x_next) / 2;

        result.push([x_cur, y_cur, cp_x1, y_cur, x_mid, y_mid]);
        result.push([x_mid, y_mid, cp_x2, y_next, x_next, y_next]);
    }
    return result;
}

function buildPolylinePoints(curveSegments, xs, ys) {
    const inetrpolateTimes = 10;
    const len = curveSegments.length;
    for (let i = 0; i < curveSegments.length; i++) {
        const segment = curveSegments[i];
        xs.push(segment[0]);
        ys.push(segment[1]);
        this.interpolatePoints(segment, xs, ys, inetrpolateTimes);
        if (i == len - 1) {
            xs.push(segment[4]);
            ys.push(segment[5]);
        }
    }
}

function drawPolyline(xs, ys, hostElement) {
    const polylineElement = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
    const len = Math.min(xs.length, ys.length);
    let pointsStr = "";
    for (let i = 0; i < len; i++) {
        pointsStr += xs[i].toFixed(3) + ',' + ys[i].toFixed(3) + ' ';
    }
    polylineElement.setAttribute('points', pointsStr);
    polylineElement.setAttribute('fill', 'none');
    polylineElement.setAttribute('stroke', '#5572c3');
    hostElement.appendChild(polylineElement);
}

function interpolatePoints(curveSegment, xs, ys, times) {
    const x_start = curveSegment[0];
    const y_start = curveSegment[1];
    const x_end = curveSegment[4];
    const y_end = curveSegment[5];
    const d = Math.pow(x_start - x_end, 2) + Math.pow(y_start - y_end, 2);
    if (d > 1) {
        for (let i = 1; i < times; i++) {
            const t = i / times;
            const x = this.deCasteljau(t, curveSegment[0], curveSegment[2], curveSegment[4]);
            const y = this.deCasteljau(t, curveSegment[1], curveSegment[3], curveSegment[5]);
            xs.push(x);
            ys.push(y);
        }
    }
}

function deCasteljau(t, p0, p1, p2) {
    return (1 - t) * ((1 - t) * p0 + t * p1) + t * ((1 - t) * p1 + t * p2);
}

const svgElement = document.getElementById("mySvg");
drawPoints(xs, ys, svgElement);
drawQuadBezierCurve(xs, ys, svgElement);

HTML

<svg id="mySvg" width="1000" height="500"></svg>

解决方案

你当前用的是二次贝塞尔曲线分段拼接,且控制点仅在水平/垂直方向取点,这种方式容易出现尖点,曲线也不够舒展。想要实现类似示例的平滑凸曲线,推荐改用三次贝塞尔曲线,并基于前后点的切线计算控制点,同时通过张力参数调整曲线凸度。

核心思路

  1. 替换二次贝塞尔为三次贝塞尔:三次贝塞尔拥有两个控制点,能更灵活地控制曲线走向与平滑度。
  2. 基于切线计算控制点:中间锚点的控制点由前后点的切线方向生成,首尾锚点则基于相邻点方向生成单侧控制点,保证曲线在锚点处切线连续,消除尖点。
  3. 张力参数调节:通过0~1之间的张力值控制曲线凸度,值越小曲线越凸越平滑,值越大曲线越贴近锚点连线。

修改后的代码

JavaScript

const xs = [942, 421, 75, 171, 479];
const ys = [406, 375, 222, 99, 38];
const tension = 0.3; // 张力参数,0~1区间,越小曲线越凸

function drawPoints(xs, ys, hostElement) {
    const len = Math.min(xs.length, ys.length);
    const r = 5;
    for (let i = 0; i < len; i++) {
        const ellipseElement = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
        ellipseElement.setAttribute('cx', xs[i]);
        ellipseElement.setAttribute('cy', ys[i]);
        ellipseElement.setAttribute('rx', r);
        ellipseElement.setAttribute('ry', r);
        ellipseElement.setAttribute('fill', "#5572c3");
        hostElement.appendChild(ellipseElement);
    }
}

function drawCubicBezierCurve(xs, ys, hostElement) {
    const pathData = buildCubicBezierPath(xs, ys);
    const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    pathElement.setAttribute('d', pathData);
    pathElement.setAttribute('fill', 'none');
    pathElement.setAttribute('stroke', '#5572c3');
    pathElement.setAttribute('stroke-width', 2);
    hostElement.appendChild(pathElement);
}

function buildCubicBezierPath(xs, ys) {
    const len = Math.min(xs.length, ys.length);
    if (len < 2) return '';
    
    let path = `M ${xs[0]} ${ys[0]}`;
    
    for (let i = 0; i < len - 1; i++) {
        const x0 = i > 0 ? xs[i - 1] : xs[i];
        const y0 = i > 0 ? ys[i - 1] : ys[i];
        const x1 = xs[i];
        const y1 = ys[i];
        const x2 = xs[i + 1];
        const y2 = ys[i + 1];
        const x3 = i < len - 2 ? xs[i + 2] : xs[i + 1];
        const y3 = i < len - 2 ? ys[i + 2] : ys[i + 1];
        
        // 计算三次贝塞尔的两个控制点
        const cp1x = x1 + (x2 - x0) * tension;
        const cp1y = y1 + (y2 - y0) * tension;
        const cp2x = x2 - (x3 - x1) * tension;
        const cp2y = y2 - (y3 - y1) * tension;
        
        path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`;
    }
    
    return path;
}

const svgElement = document.getElementById("mySvg");
drawPoints(xs, ys, svgElement);
drawCubicBezierCurve(xs, ys, svgElement);

HTML

<svg id="mySvg" width="1000" height="500"></svg>

代码说明

  • 直接使用SVG的path元素:原生支持三次贝塞尔曲线的C指令,无需手动插值点,渲染效率更高且曲线更平滑。
  • 控制点逻辑:每个锚点的前控制点基于前点与当前点的方向生成,后控制点基于当前点与下点的方向生成,确保曲线在锚点处的切线连续,彻底消除尖点。
  • 张力可调:修改tension值即可快速调整曲线凸度,比如设为0.2会更凸,设为0.5会更贴近锚点连线。

内容的提问来源于stack exchange,提问作者jena_kk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 18:07:02