Suppose I want to approximate a half-cosine curve in SVG using bezier paths. The half cosine should look like this:
![](https://www.manongdao.com/static/images/pcload.jpg)
and runs from [x0,y0] (the left-hand control point) to [x1,y1] (the right-hand one).
How can I find an acceptable set of coefficients for a good approximation of this function?
Bonus question: how is it possible to generalize the formula for, for example, a quarter of cosine?
Please note that I don't want to approximate the cosine with a series of interconnected segments, I'd like to calculate a good approximation using a Bezier curve.
I tried the solution in comments, but, with those coefficients, the curve seems to end after the second point.
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.
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:
(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.
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.,
.
Since the derivative of our Bezier curve is given by
, this simplifies to
at the point t=0.5.
Setting this equal to our desired derivative, we obtain the solution ![](https://www.manongdao.com/static/images/pcload.jpg)
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:
![](https://www.manongdao.com/static/images/pcload.jpg)
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:
![](https://www.manongdao.com/static/images/pcload.jpg)
where B is defined as follows:
![](https://www.manongdao.com/static/images/pcload.jpg)
cubic-bezier(0.364212423249, 0, 0.635787576751, 1)
I would recommend reading this article on the math of bezier curves and ellipses, as this is basicly what you want (draw a part of an ellipse):
http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
it provides some of the insights required.
then look at this graphic:
http://www.svgopen.org/2003/papers/AnimatedMathematics/ellipse.svg
where an example is made for an ellipse
now that you get the math involved, please see this example in LUA ;)
http://commons.wikimedia.org/wiki/File:Harmonic_partials_on_strings.svg
tada...