如何选择控制点绘制更平滑的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>
解决方案
你当前用的是二次贝塞尔曲线分段拼接,且控制点仅在水平/垂直方向取点,这种方式容易出现尖点,曲线也不够舒展。想要实现类似示例的平滑凸曲线,推荐改用三次贝塞尔曲线,并基于前后点的切线计算控制点,同时通过张力参数调整曲线凸度。
核心思路
- 替换二次贝塞尔为三次贝塞尔:三次贝塞尔拥有两个控制点,能更灵活地控制曲线走向与平滑度。
- 基于切线计算控制点:中间锚点的控制点由前后点的切线方向生成,首尾锚点则基于相邻点方向生成单侧控制点,保证曲线在锚点处切线连续,消除尖点。
- 张力参数调节:通过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
相关产品推荐
相关产品推荐

