Las curvas Bézier son una forma conveniente de aproximar curvas con polinomios, y están presentes en todos los lenguajes de programación que permiten dibujar gráficos que he probado. Lo normal es que el lenguaje proporcione nativamente métodos para aproximar por polinomios cuadráticos o cúbicos.

Trataré de ilustrar el ejemplo de aproximar una sinusoide por una sucesión de curvas Bézier cuadráticas (polinomios de segundo grado), ya que la forma de una onda de este tipo es simétrica respecto al punto medio entre dos nodos (puntos de corte con el eje X). Partimos de la forma general de dicha onda:


Para cada cresta y cada valle necesitaremos tres puntos:
  • El punto de partida P0i = (x0i, y0i), que coincidirá con el de llegada P2i-1 de la cresta o valle anterior.
  • Un punto de control P1i = (x1i, y1i), que será la intersección de las rectas tangentes a la curva original en los puntos de origen P0i y destino P2i.
  • El punto de destino P2i = (x2i, y2i), que coincidirá con el punto de origen P0i+1 de la cresta o valle posterior.
Para simplificar el problema, lo resolveremos en el intervalo [0,1], y con una amplitud A de 1. Siempre podremos escalar, trasladar o rotar la figura resultante como nos convenga aplicando a cada punto una transformación lineal.

Los puntos de partida y de destino serán los que tome la función en 0 (el primero de partida) y en 1 (el último de llegada), y los nodos (los intermedios). Los nodos intermedios serán aquellos puntos de la forma



La última condición restringe los valores de :

Si , será ,
Si , será ,

Los puntos de control serán la intersección de las rectas tangentes en cada par de puntos de partida y destino. Para obtenerlos deberemos diferenciar la ecuación (1) en cada punto y hallar la intersección de las rectas con cada pendiente que pasan por cada punto.

La obtención de los puntos de origen y destino y de los de control, se puede resumir en el resultado de la siguiente función (javascript):

Código:
 function buildCurve() {
    const points = [];
    const control = [];

    let last = {x: 0, y: sinFn(0)};
    let lastDydx = diff(last.x);
    points.push(last);

    const firstNode = Math.PI < delta ? 2 : 1;
    const lastNode = k / Math.PI + firstNode - 1;
    for (n = firstNode; n <= lastNode; n++) {
        const x2 = (Math.PI * n - delta) / k;
        const y2 = 0;
        const curr = {x: x2, y: y2};
        const currentDydx = diff(curr.x);
        const controlX = (curr.y - last.y - (curr.x * currentDydx - last.x * lastDydx)) / (lastDydx - currentDydx);
        const controlY = last.y - lastDydx * last.x + lastDydx * controlX;
        const cont = {x: controlX, y: controlY};
        last = curr;
        lastDydx = currentDydx;
        points.push(last);
        control.push(cont);
    }

    const curr = {x: 1, y: sinFn(1)};
    const currentDydx = diff(curr.x);
    const controlX = (curr.y - last.y - (curr.x * currentDydx - last.x * lastDydx)) / (lastDydx - currentDydx);
    const controlY = last.y - lastDydx * last.x + lastDydx * controlX;
    const cont = {x: controlX, y: controlY};
    points.push(curr);
    control.push(cont);

    return {points: points, control: control};
}
Donde se han usado las funciones siguientes, definiendo arbitrariamente y el número de ondas :

Código:
const delta = 0.5;
const k = 2 * Math.PI * 3;
function sinFn(x) {
    return Math.sin(k*x+delta);
}
function diff(x) {
    return k*Math.cos(k*x+delta);
}
Con esto, la función que finalmente dibujaría la curva tendría este aspecto:

Código:
function paintFig1(context) {
    const curveData = buildCurve();
    const points = curveData.points.map(p => toCanvasCoordinates(context, p));
    const control = curveData.control.map(p => toCanvasCoordinates(context, p));
    context.beginPath();
    context.moveTo(points[0].x, points[0].y);
    for (let i = 0; i < control.length; i++) {
        context.quadraticCurveTo(control[i].x, control[i].y, points[i+1].x, points[i+1].y);
    }
    context.stroke();
}
Por lo que, con las transformaciones correspondientes resumidas en la función toCanvasCoordinates para que ocupe todo el área de dibujo, queda el siguiente gráfico:
Aproximación de la sinusoide con curvas Bézier





En él se muestra cómo queda la curva en negro y los puntos de control en verde, comprobándose que son la intersección entre las tangentes de los nodos.

Comparando el resultado con la solución exacta (en rojo):
Comparación de la aproximación con la forma exacta





Se comprueba que la precisión en las crestas y los valles deja mucho que desear, es más, la primera cresta parece más alta de lo que sería de esperar debido al desfase introducido, ya que al estar el punto por el que ha de pasar la curva más cerca de la cresta se aproxima por ese lado a la solución exacta. Esto último se puede solucionar eliminando los puntos inicial y final y añadiendo los nodos más cercanos, pero requeriría recortar una ventana del lienzo, limpiarlo y volverla a pintar. Lo primero puede solucionarse añadiendo los máximos y mínimos como puntos por los que debe pasar la curva. Naturalmente, cuantos más puntos de paso tenga la aproximación, mejor va a ser esta.

Sin embargo, para la mayoría de aplicaciones de dibujo, en las que la precisión no es tan importante como la representatividad, y en la que muchas veces los errores de precisión puedan ser hasta refrescantes, este método es rápido y eficaz.