如何在 SVG 中使用贝塞尔路径逼近半余弦曲线?

How to approximate a half-cosine curve with bezier paths in SVG?

假设我想在 SVG 中使用贝塞尔路径来近似半余弦曲线。半余弦应如下所示:

从[x0,y0](左侧控制点)到[x1,y1](右侧控制点)。

如何找到一组可接受的系数来很好地近似此函数?

奖金问题:如何推广公式,例如,余弦的四分之一?

请注意,我不想用一系列相互连接的段来近似余弦,我想使用贝塞尔曲线计算一个好的近似值。

我尝试了评论中的解决方案,但是,使用这些系数,曲线似乎在第二个点之后结束。

经过几次tries/errors,我发现正确的比例是K=0.37.

"M" + x1 + "," + y1
+ "C" + (x1 + K * (x2 - x1)) + "," + y1 + ","
+ (x2 - K * (x2 - x1)) + "," + y2 + ","
+ x2 + "," + y2

查看此示例以了解贝塞尔曲线如何与余弦匹配:http://jsfiddle.net/6165Lxu6/

绿线是实余弦,黑线是贝塞尔曲线。向下滚动以查看 5 个示例。每次刷新点都是随机的。

为了泛化,我建议使用裁剪。

我建议阅读这篇关于贝塞尔曲线和椭圆数学的文章,因为这基本上就是您想要的(绘制椭圆的一部分): http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf

它提供了一些所需的见解。

然后看这张图: http://www.svgopen.org/2003/papers/AnimatedMathematics/ellipse.svg

其中为椭圆制作示例

现在您已经掌握了数学知识,请参阅 LUA 中的示例 ;) http://commons.wikimedia.org/wiki/File:Harmonic_partials_on_strings.svg

多田...

假设您希望两端的切线保持水平。所以自然地,解决方案将是对称的,归结为在水平方向上找到第一个控制点。

我写了一个程序来做这个:

/*
* Find the best cubic Bézier curve approximation of a sine curve.
*
* We want a cubic Bézier curve made out of points (0,0), (0,K), (1-K,1), (1,1) that approximates
* the shifted sine curve (y = a⋅sin(bx + c) + d) which has its minimum at (0,0) and maximum at (1,1).
* This is useful for CSS animation functions.
*
*      ↑      P2         P3
*      1      ו••••••***×
*      |           ***
*      |         **
*      |        *
*      |      **
*      |   ***
*      ×***•••••••×------1-→
*      P0         P1
*/

const sampleSize = 10000; // number of points to compare when determining the root-mean-square deviation
const iterations = 12; // each iteration gives one more digit

// f(x) = (sin(π⋅(x - 1/2)) + 1) / 2 = (1 - cos(πx)) / 2
const f = x => (1 - Math.cos(Math.PI * x)) / 2;

const sum = function (a, b, c) {
  if (Array.isArray(c)) {
      return [...arguments].reduce(sum);
  }
  return [a[0] + b[0], a[1] + b[1]];
};

const times = (c, [x0, x1]) => [c * x0, c * x1];

// starting points for our iteration
let [left, right] = [0, 1];
for (let digits = 1; digits <= iterations; digits++) {
    // left and right are always integers (digits after 0), this keeps rounding errors low
    // In each iteration, we divide them by a higher power of 10
    let power = Math.pow(10, digits);
    let min = [null, Infinity];
    for (let K = 10 * left; K <= 10 * right; K+= 1) { // note that the candidates for K have one more digit than previous `left` and `right`
        const P1 = [K / power, 0];
        const P2 = [1 - K / power, 1];
        const P3 = [1, 1];

        let bezierPoint = t => sum(
            times(3 * t * (1 - t) * (1 - t), P1),
            times(3 * t * t * (1 - t), P2),
            times(t * t * t, P3)
        );

        // determine the error (root-mean-square)
        let squaredErrorSum = 0;
        for (let i = 0; i < sampleSize; i++) {
            let t = i / sampleSize / 2;
            let P = bezierPoint(t);
            let delta = P[1] - f(P[0]);
            squaredErrorSum += delta * delta;
        }
        let deviation = Math.sqrt(squaredErrorSum); // no need to divide by sampleSize, since it is constant

        if (deviation < min[1]) {
            // this is the best K value with ${digits + 1} digits
            min = [K, deviation];
        }
    }
    left = min[0] - 1;
    right = min[0] + 1;
    console.log(`.${min[0]}`);
}

为了简化计算,我使用了归一化的正弦曲线,它通过(0,0)(1,1)作为它的最小/最大点。这对于 CSS 动画也很有用。

它returns(.3642124232,0)*作为均方根偏差最小的点(关于 0.00013).

我还创建了一个 Desmos graph 来显示准确性:

(点击试试——可以左右拖动控制点)


*注意用JS做数学运算时有舍入误差,所以估计精度不超过5位左右

由于贝塞尔曲线无法准确重建正弦曲线,因此有多种方法可以创建近似值。我将假设我们的曲线从点 (0, 0) 开始并在 (1, 1) 结束。

简单方法

解决这个问题的一个简单方法是用控制点 (K, 0) 和 ((1 - K), 1) 构造贝塞尔曲线 B 因为所涉及的对称性以及在 t=0 和 t=1 处保持水平切线的愿望。

然后我们只需要找到一个 K 值,使我们的贝塞尔曲线的导数与 t=0.5 处的正弦曲线的导数相匹配,即 pi / 2.

由于 derivative of our Bezier curve\frac{dy}{dx} = \frac{dy/dt}{dx/dt} = \frac{d(3(1-t)t^2+t^3)/dt}{d(3(1-t)^2tK+3(1-t)t^2(1-K)+t^3)/dt} 给出,这在 t=0.5 时简化为 d

将其设置为我们想要的导数,我们得到解决方案K=\frac{\pi-2}{\pi}\approx0.36338022763241865692446494650994255\ldots

因此,我们的近似结果是:

cubic-bezier(0.3633802276324187, 0, 0.6366197723675813, 1)

它非常接近,均方根偏差约为 0.000224528:

高级方法

为了获得更好的近似值,我们可能希望最小化它们的差异 root mean square。这计算起来比较复杂,因为我们现在试图在区间 (0, 1) 中找到使以下表达式最小化的 K 值:

其中 B 定义如下:

cubic-bezier(0.364212423249, 0, 0.635787576751, 1)