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

Let's assume you want to keep the tangent horizontal on both ends. So naturally the solution is going to be symmetric, and boils down to finding a first control point in horizontal direction.

I wrote a program to do this:

/*
* 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]}`);
}

To simplify calculations, I use the normalized sine curve, which passes through (0,0) and (1,1) as its minimal / maximal points. This is also useful for CSS animations.

It returns (.3642124232,0)* as the point with the smallest root-mean-square deviation (about 0.00013).

I also created a Desmos graph that shows the accuracy:

Desmos Graph (sine approximation with cubic Bézier curve) (Click to try it out - you can drag the control point left and right)


* Note that there are rounding errors when doing math with JS, so the value is presumably accurate to no more than 5 digits or so.


After few tries/errors, I found that the correct ratio is K=0.37.

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

Look at this samples to see how Bezier matches with cosine: http://jsfiddle.net/6165Lxu6/

The green line is the real cosine, the black one is the Bezier. Scroll down to see 5 samples. Points are random at each refresh.

For the generalization, I suggest to use clipping.


Because a Bezier curve cannot exactly reconstruct a sinusoidal curve, there are many ways to create an approximation. I am going to assume that our curve starts at the point (0, 0) and ends at (1, 1).

Simple method

A simple way to approach this problem is to construct a Bezier curve B with the control points (K, 0) and ((1 - K), 1) because of the symmetry involved and the desire to keep a horizontal tangent at t=0 and t=1.

Then we just need to find a value of K such that the derivative of our Bezier curve matches that of the sinusoidal at t=0.5, i.e., pi / 2.

Since the derivative of our Bezier curve is given by \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}, this simplifies to d at the point t=0.5.

Setting this equal to our desired derivative, we obtain the solution K=\frac{\pi-2}{\pi}\approx0.36338022763241865692446494650994255\ldots

Thus, our approximation results in:

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

and it comes very close with a root mean square deviation of about 0.000224528:

cubic bezier approximation compared with sinusoidal

Advanced Method

For a better approximation, we may want to minimize the root mean square of their difference instead. This is more complicated to calculate, as we are now trying to find the value of K in the interval (0, 1) that minimizes the following expression:

\int ^{1}_{0} \left( B_y\left(t\right) - \frac{ 1 - \cos \left( \pi B_x \left( t \right) \right) }{2} \right)^2 B_x'\left( t \right) dt

where B is defined as follows:

B_x(t) = 3\left(1-t\right)^2tK+3\left(1-t\right)t^2\left(1-K\right)+t^3; B_y(t) = 3\left(1-t\right)t^2+t^3

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