如何使用JavaScript HTML5画布通过N个点绘制平滑曲线?

172
对于一个绘图应用程序,我将鼠标移动坐标保存到数组中,然后使用lineTo进行绘制。 产生的线条不够平滑。如何在所有收集到的点之间产生一个单一的曲线?
我搜索了谷歌,但只找到三种画线的函数:对于2个样本点,简单地使用lineTo;对于3个样本点,使用quadraticCurveTo;对于4个样本点,使用bezierCurveTo
(我尝试在数组中每4个点绘制一个bezierCurveTo,但这会导致四个采样点时出现折痕,而不是连续平滑的曲线)。
如何编写一个功能以绘制5个采样点及更多的平滑曲线?

5
“smooth”指的是什么?无限可微分?二阶可导?立方样条曲线(“贝塞尔曲线”)具有许多良好的特性,且二阶可导,计算也相对简单。 - Kerrek SB
10
“smooth”在此指外观上无法检测到任何角落/尖点等。 - Homan
@sketchfemme,你是实时渲染线条,还是等收集一堆点后再渲染? - Crashalot
@Crashalot 我正在将点收集到一个数组中。你需要至少4个点才能使用这个算法。之后,你可以在每次mouseMove调用时清除屏幕,在画布上实时渲染。 - Homan
2
@sketchfemme:不要忘记接受一个答案。如果是你自己的也没关系 - T.J. Crowder
你一定要看看 fit-curve.js ... 这里还有一个 演示页面 - ashleedawg
14个回答

2

这段代码非常适合我:

this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
  this.context.bezierCurveTo(
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i - 1].y,
    data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
    data[i].y,
    data[i].x,
    data[i].y);
}

您需要正确的平滑线和正确的端点。 注意!(y =“画布高度”- y);


非常好,谢谢。但是你错过了在最后绘制线条的实际命令:this.context.stroke()。 - Michael S.

0
为了补充K3N的基数样条方法并可能解决T.J.Crowder对曲线在误导性位置“下降”的担忧,我在getCurvePoints()函数中,在res.push(x);之前插入了以下代码。
if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) {
    y = (_pts[i+1] + _pts[i+3]) / 2;
}
if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) {
    x = (_pts[i] + _pts[i+2]) / 2;
}

这实际上在每对相邻点之间创建了一个(不可见的)边界框,并确保曲线保持在此边界框内 - 即,如果曲线上的某个点在两个点的上方/下方/左侧/右侧,则会改变其位置以使其位于该框内。这里使用中点,但可以进行改进,例如使用线性插值。


0
如果您想确定通过n个点的曲线方程,则以下代码将为您提供n-1次多项式的系数,并将这些系数保存到coefficients[]数组中(从常数项开始)。x坐标不必按顺序排列。这是Lagrange polynomial的一个示例。
var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
    for (var m=0; m<xPoints.length; m++) {
        var newCoefficients=[];
        for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
        if (m>0) {
            newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
            newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
    } else {
        newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
        newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
    }
    var startIndex=1; 
    if (m==0) startIndex=2; 
    for (var n=startIndex; n<xPoints.length; n++) {
        if (m==n) continue;
        for (var nc=xPoints.length-1; nc>=1; nc--) {
        newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
        }
        newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
    }    
    for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}

0

我需要一种只使用二次贝塞尔曲线的方法。这是我的方法,可以扩展到3D:

二次贝塞尔曲线的公式为

b(t) = (1-t)^2A + 2(1-t)tB + t^2*C

当t=0或1时,曲线可以通过点A或C,但不能保证通过B。

它的一阶导数是

b'(t) = 2(t-1)A + 2(1-2t)B + 2tC

为了用两个二次贝塞尔曲线构建通过点P0、P1、P2的曲线,两个贝塞尔曲线在P1处的斜率应该相等。

编程相关内容翻译: 代码如下: ``` α(t) = 2(t-1)P0 + 2(1-2t)M1 + 2tP1 β(t) = 2(t-1)P1 + 2(1-2t)M2 + 2tP2 α(1) = β(0) ``` 解释如下: 这段代码可以得到以下结果:
``` (M1 + M2) / 2 = P1 ```
因此,可以通过这种方式绘制通过三个点的曲线。
bezier(p0, m1, p1);
bezier(p1, m2, p2);

m1p1 = p1m2,其中m1m2的方向不重要,可以通过p2 - p1找到。

对于穿过4个或更多点的曲线

bezier(p0, m1, p1);
bezier(p1, m2, (m2 + m3) / 2);
bezier((m2 + m3) / 2, m3, p2);
bezier(p2, m4, p3);

其中 m1p1 = p1m2m3p2 = p2m4

function drawCurve(ctx: CanvasRenderingContext2D, points: { x: number, y: number }[], tension = 2) {
    if (points.length < 2) {
        return;
    }
    ctx.beginPath();
    if (points.length === 2) {
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[1].x, points[1].y);
        ctx.stroke();
        return;
    }
    let prevM2x = 0;
    let prevM2y = 0;
    for (let i = 1, len = points.length; i < len - 1; ++i) {
        const p0 = points[i - 1];
        const p1 = points[i];
        const p2 = points[i + 1];
        let tx = p2.x - (i === 1 ? p0.x : prevM2x);
        let ty = p2.y - (i === 1 ? p0.y : prevM2y);
        const tLen = Math.sqrt(tx ** 2 + ty ** 2);
        if (tLen > 1e-8) {
            const inv = 1 / tLen;
            tx *= inv;
            ty *= inv;
        } else {
            tx = 0;
            ty = 0;
        }
        const det = Math.sqrt(Math.min(
            (p0.x - p1.x) ** 2 + (p0.y - p1.y) ** 2,
            (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
        )) / (2 * tension);
        const m1x = p1.x - tx * det;
        const m1y = p1.y - ty * det;
        const m2x = p1.x + tx * det;
        const m2y = p1.y + ty * det;
        if (i === 1) {
            ctx.moveTo(p0.x, p0.y);
            ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
        } else {
            const mx = (prevM2x + m1x) / 2;
            const my = (prevM2y + m1y) / 2;
            ctx.quadraticCurveTo(prevM2x, prevM2y, mx, my);
            ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
        }
        if (i === len - 2) {
            ctx.quadraticCurveTo(m2x, m2y, p2.x, p2.y);
        }
        prevM2x = m2x;
        prevM2y = m2y;
    }
    ctx.stroke();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接