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

4

22

Suppose I want to approximate a half-cosine curve in SVG using bezier paths. The half cosine should look like this:

half cosine

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.

Medellin answered 12/3, 2015 at 23:46 Comment(6)
possible duplicate of How to draw sine waves with SVG (+JS)?Landlordism
Paul LeBeau is right - have a look at the answer dealing with Bézier control pointsHanrahan
I don't honestly know to apply the answer. Let's say I use a cubic bezier with 0,0 1/2,1/2 1,1 π/2,1, I tried something like: 'M' + x0 + "," + y0 + ' C' + x0 * 0.5 + ',' + y0 * 0.5 + ' ' + x1 * 1 + ',' + y1 * 1 + ' ' + x1 * Math.PI / 2 + ',' + y1 * 1;, but it obvioulsy goes past my right point.Medellin
@ThomasW I see you're one of the answerer there, what am I missing about the control points? A cubic bezier has the origin point, two control points and the end point. What should be the control points be, given start and end?Medellin
All four points (including start and end point) are called control points.Hanrahan
@ThomasW: ok, so shouldn't the last points be 1,1 instead of π/2,1? I tried to draw a Bezier with those constrol points and obviously it goes over the end point on the right. I'm just trying to understand how to scale those control points to make a bezier approximation that starts with the start piunt and ends at the end point.Medellin
O
11

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.

Organon answered 23/3, 2015 at 15:51 Comment(0)
T
24

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.

Tollbooth answered 2/8, 2017 at 4:12 Comment(0)
B
17

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)
Blanco answered 30/5, 2018 at 5:30 Comment(4)
And using Casteljau's method I obtain the quarter-cosine spline as M 0,0 C 0.18169,0 0.340845,0.25 0.5,0.5 where the two numbers are K/2 and (K+1)/4, respectivelySwore
And the quarter-cosine with the RMS K is M 0,0 C 0.182106,0 0.341053,0.25 0.5,0.5Swore
Having the same derivative in .5 seems to "make sense", but is there any mathematical reason why this would yield a good approximation?Tollbooth
ThomasR, With three points, we have an approximation and unique solution that is able to match both the points and "slopes" (derivative) at t=0, t=0.5, and t=1. Having two points at t=0, t=1, we wouldn't have a unique solution. At all three points, the lines cross in the same direction and meet at the same points.Blanco
O
11

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.

Organon answered 23/3, 2015 at 15:51 Comment(0)
H
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...

Herv answered 24/3, 2015 at 14:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.