La longitud de arco de una curva de Bézier es la distancia total que recorre un punto desde el inicio de la curva hasta el final. Se obtiene integrando la rapidez de ese punto, que es la longitud de la derivada de la curva, a lo largo del rango del parámetro. Una curva de Bézier cuadrática tiene una forma cerrada exacta. Una curva de Bézier cúbica por lo general no la tiene, así que las implementaciones aproximan su longitud con cuadratura numérica. Esta página deriva la fórmula cuadrática, construye la aproximación cúbica y muestra cómo convertir la longitud de arco en una parametrización por longitud que mueve un punto a rapidez constante. El tutorial interactivo de curvas de Bézier cubre la geometría de los puntos de control sobre la que se construye todo lo de aquí.
La longitud de arco es la integral de la rapidez
Una curva de Bézier asigna un punto a cada valor del parámetro entre y . A medida que aumenta, ese punto se desliza a lo largo de la curva. Su velocidad es la derivada , un vector que apunta en la dirección del movimiento, y la rapidez es la longitud de ese vector:
La rapidez mide la distancia recorrida por unidad de . La rapidez por un paso infinitesimal da la distancia cubierta durante ese paso, así que sumar esas piezas pequeñas da el total:
Aquí es el parámetro de la curva, es una variable muda de integración, es la longitud de arco acumulada desde el inicio hasta el parámetro , y es la longitud total de la curva. La línea recta entre los extremos siempre es más corta que la curva, y el polígono de control, que conecta los puntos de control con segmentos rectos, siempre es más largo. La longitud verdadera se encuentra entre esos dos límites, y es igual al área bajo la curva de rapidez representada frente a .
La consecuencia práctica importante es que no es distancia. Pasos iguales en producen pasos desiguales en distancia siempre que la rapidez cambie a lo largo de la curva. Un renderizador que muestrea de manera uniforme en colocará más puntos donde la curva es lenta y menos donde es rápida.
El panel izquierdo es una curva cuadrática o cúbica editable. El panel derecho representa la rapidez correspondiente frente a y sombrea el área bajo ella. Esa área sombreada es la longitud de arco impresa en la esquina. Arrastrar un punto de control cambia ambos paneles a la vez: separar los puntos de control eleva la curva de rapidez y agranda el área, mientras que una forma compacta baja la curva y la reduce. Cambia al caso cúbico y la curva de rapidez puede desarrollar varias jorobas, porque la derivada de una cúbica es un polinomio cuadrático cuya longitud cambia con más libertad que la derivada lineal de una cuadrática.
Encontrar la longitud total de una curva de Bézier cuadrática
Una curva de Bézier cuadrática usa tres puntos y la fórmula
Derivar da la velocidad:
Esa expresión es lineal en , que es la clave de la forma cerrada. Agrupa las partes constante y lineal definiendo dos vectores nuevos:
Ahora la velocidad es simplemente
Puedes comprobarlo expandiendo : es igual a , la derivada de arriba. Como la derivada es una línea recta en , su longitud al cuadrado es un polinomio cuadrático:
con los tres coeficientes escalares
La rapidez es la raíz cuadrada de ese polinomio cuadrático, y la longitud es su integral:
Para evaluarla, completa el cuadrado dentro de la raíz. Escribe y . Entonces
Sustituir convierte la integral en una forma estándar:
La primitiva de para es
así que la longitud de arco cuadrática exacta es
La cantidad nunca es negativa para una curva de Bézier cuadrática. El producto nunca puede superar a por la desigualdad de Cauchy-Schwarz, y esa es exactamente la condición que hace que . Cuando y son paralelos, es cero, el logaritmo se vuelve singular y la raíz se simplifica a . La integral del valor absoluto es fácil de evaluar por tramos.
Ejemplo desarrollado
Toma , y . Entonces
Los coeficientes son , y . El desplazamiento es
y el término constante es
Evaluar en los dos extremos da y . Con , la longitud es
Longitud de arco cuadrática en código
Esta función devuelve la longitud exacta, incluidos los casos de línea recta y de velocidad paralela:
function quadraticArcLength(P0, P1, P2) {
// A = P0 - 2 P1 + P2 and B = 2 (P1 - P0) make B'(t) = 2 A t + B.
const ax = P0.x - 2 * P1.x + P2.x;
const ay = P0.y - 2 * P1.y + P2.y;
const bx = 2 * (P1.x - P0.x);
const by = 2 * (P1.y - P0.y);
const a = 4 * (ax * ax + ay * ay);
const b = 4 * (ax * bx + ay * by);
const c = bx * bx + by * by;
// Constant speed: the curve is a straight line.
if (a === 0) return Math.sqrt(c);
const k = b / (2 * a);
const scale = Math.sqrt(a);
const disc = b * b - 4 * a * c;
// A parallel to B: the speed is sqrt(a) * |t + k|.
if (Math.abs(disc) < 1e-12 * a * c + 1e-30) {
const abs = (x) => 0.5 * x * Math.abs(x);
return scale * (abs(1 + k) - abs(k));
}
const m = c / a - k * k;
const F = (u) => 0.5 * (u * Math.sqrt(u * u + m) + m * Math.log(u + Math.sqrt(u * u + m)));
return scale * (F(1 + k) - F(k));
}
La rama a === 0 se activa cuando es el punto medio de y , lo que colapsa la curva en una línea recta. La rama de discriminante casi nulo captura la situación en la que la velocidad permanece sobre una misma recta pero puede invertir su dirección, de modo que la curva va y vuelve. Para el caso común, la forma cerrada se ejecuta en tiempo constante sin muestreo.
Por qué la longitud de arco de una Bézier cúbica no tiene forma cerrada
Una curva de Bézier cúbica usa cuatro puntos:
Su derivada es
Cada término es un vector multiplicado por un polinomio cuadrático en , así que es un vector de grado dos, y es un único polinomio de cuarto grado en . La rapidez es la raíz cuadrada de ese polinomio de cuarto grado. Las integrales de la raíz cuadrada de un cuarto grado general son integrales elípticas, y las integrales elípticas no tienen expresión en funciones elementales. No existe una fórmula logarítmica o con raíces que cubra todas las cúbicas como la fórmula de completar el cuadrado cubre todas las cuadráticas.
Eso no significa que la longitud sea incognoscible. Significa que la longitud debe calcularse en lugar de consultarse, y la herramienta estándar para ese trabajo es la cuadratura gaussiana.
Aproximar la longitud de arco de una Bézier cúbica
La cuadratura de Gauss-Legendre aproxima una integral definida muestreando el integrando en un conjunto pequeño de puntos y tomando una suma ponderada:
Los puntos son las raíces del -ésimo polinomio de Legendre, y los pesos se eligen de modo que la regla sea exacta para todos los polinomios hasta el grado . Nuestro intervalo es en lugar de , así que sustituye , lo que da
Dieciséis puntos hacen que la regla sea exacta para polinomios hasta el grado 31. La rapidez es una raíz cuadrada en lugar de un polinomio, así que la regla no es exacta, pero para una función de rapidez suave 16 puntos ya quedan dentro de unas pocas partes por millón de la longitud verdadera.
// 16-point Gauss-Legendre abscissae and weights on [-1, 1].
const GL_X = [
-0.989400934991650, -0.944575023073233, -0.865631202387832, -0.755404408355003,
-0.617876244402644, -0.458016777657227, -0.281603550779259, -0.095012509837637,
0.095012509837637, 0.281603550779259, 0.458016777657227, 0.617876244402644,
0.755404408355003, 0.865631202387832, 0.944575023073233, 0.989400934991650,
];
const GL_W = [
0.027152459411754, 0.062253523938648, 0.095158511682493, 0.124628971255534,
0.149595988816577, 0.169156519395003, 0.182603415044924, 0.189450610455069,
0.189450610455069, 0.182603415044924, 0.169156519395003, 0.149595988816577,
0.124628971255534, 0.095158511682493, 0.062253523938648, 0.027152459411754,
];
function cubicSpeed(t, P) {
const u = 1 - t;
const dx = 3 * u * u * (P[1].x - P[0].x) + 6 * u * t * (P[2].x - P[1].x) + 3 * t * t * (P[3].x - P[2].x);
const dy = 3 * u * u * (P[1].y - P[0].y) + 6 * u * t * (P[2].y - P[1].y) + 3 * t * t * (P[3].y - P[2].y);
return Math.sqrt(dx * dx + dy * dy);
}
function cubicArcLength(P) {
let sum = 0;
for (let i = 0; i < GL_X.length; i++) {
const t = 0.5 * GL_X[i] + 0.5;
sum += GL_W[i] * cubicSpeed(t, P);
}
return 0.5 * sum;
}
La misma función auxiliar sirve cuando la cuadratura necesita la longitud desde el inicio hasta un parámetro arbitrario en lugar de la curva completa. Conserva las abscisas y los pesos, y escala el intervalo a :
function cubicArcLengthTo(t, P) {
if (t <= 0) return 0;
let sum = 0;
for (let i = 0; i < GL_X.length; i++) {
const s = 0.5 * t * (GL_X[i] + 1);
sum += GL_W[i] * cubicSpeed(s, P);
}
return 0.5 * t * sum;
}
¿Qué precisión tiene la cuadratura?
La tabla compara la regla con una referencia de alta precisión para tres curvas cúbicas. Los errores son absolutos.
| Puntos de control de la cúbica | Longitud verdadera | 8 puntos | 16 puntos | 24 puntos |
|---|---|---|---|---|
| 6.214707537 | ||||
| 3.443380724 | ||||
| 1.154700538 |
Las dos primeras curvas tienen perfiles de rapidez suaves, y 16 puntos son más que suficientes. La tercera curva se regresa sobre sí misma, así que su rapidez cae a cero y la función de rapidez tiene una esquina. La cuadratura converge lentamente en una esquina, y añadir más puntos apenas ayuda. La solución es dividir el rango del parámetro en el punto problemático e integrar cada pieza por separado, o usar el método de subdivisión de abajo.
La subdivisión adaptativa como alternativa
El algoritmo de De Casteljau puede dividir un segmento cúbico en dos segmentos cúbicos más pequeños en cualquier valor del parámetro, y dividir en es especialmente simple: promedia puntos de control vecinos hasta que quede un solo punto. El polígono de control de una pieza pequeña es casi una línea recta, así que tanto su longitud como la longitud de la cuerda entre extremos se acercan a la longitud de arco verdadera. La diferencia entre ambas mide cuánto se curva todavía la pieza. Cuando la diferencia queda por debajo de una tolerancia, devuelve el promedio de las dos; en caso contrario, divide y recurre.
const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
const midpoint = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
function adaptiveArcLength(P0, P1, P2, P3, tolerance = 1e-6) {
const chord = distance(P0, P3);
const polygon =
distance(P0, P1) + distance(P1, P2) + distance(P2, P3);
if (polygon - chord <= tolerance) {
return (polygon + chord) / 2;
}
// Split at t = 0.5 with de Casteljau averaging.
const p01 = midpoint(P0, P1);
const p12 = midpoint(P1, P2);
const p23 = midpoint(P2, P3);
const p012 = midpoint(p01, p12);
const p123 = midpoint(p12, p23);
const mid = midpoint(p012, p123);
return (
adaptiveArcLength(P0, p01, p012, mid, tolerance) +
adaptiveArcLength(mid, p123, p23, P3, tolerance)
);
}
La subdivisión adaptativa cuesta más evaluaciones que una sola regla de 16 puntos, pero concentra el trabajo donde la curva se dobla y sigue funcionando en curvas con esquinas o casi cúspides. La regla de cuadratura es más rápida para las curvas suaves que aparecen en la mayoría del trabajo de renderizado y animación, y la subdivisión es el respaldo seguro cuando hay que garantizar una tolerancia.
Parametrización por longitud de una curva de Bézier
Todo lo anterior calcula la longitud a partir del parámetro. La pregunta inversa, encontrar el parámetro que corresponde a una distancia dada, es lo que hace posible el movimiento a rapidez constante. Define la función de longitud acumulada
Como la rapidez nunca es negativa, aumenta de forma constante de a a medida que va de a , así que puede invertirse. La inversa, escrita , es la parametrización por longitud: dale una distancia y devuelve el parámetro que sitúa el punto exactamente a esa distancia a lo largo de la curva. La dirección directa tiene una forma cerrada para curvas cuadráticas y una regla de cuadratura para curvas cúbicas, pero ninguna de las dos inversas tiene una expresión elemental en general, así que las implementaciones construyen una tabla e interpolan.
La receta tiene tres pasos. Muestrea en muchos valores de uniformemente espaciados y guarda los pares. Para convertir una distancia objetivo en un parámetro, busca binariamente en la tabla el par que la rodea y luego interpola linealmente. Como la tabla es solo una aproximación lineal a trozos de una función suave, termina con uno o dos pasos de Newton. El método de Newton encuentra una raíz de usando la derivada , que es exactamente la rapidez que ya está disponible.
Ambos paneles muestran la misma curva cúbica. El panel izquierdo coloca un punto cada décima parte del rango del parámetro, y el panel derecho coloca un punto cada décima parte de la longitud de arco total. Arrastrar un punto de control remodela ambos paneles a la vez. Donde la curva se mueve despacio, los puntos del parámetro se agolpan, y donde se mueve rápido, se separan. Los puntos de longitud de arco permanecen uniformemente espaciados porque la tabla asigna cada distancia igual al parámetro que la alcanza. Los marcadores rojos recorren ambas curvas en el mismo tiempo. El marcador izquierdo acelera y frena con el parámetro, y el marcador derecho se mueve a un ritmo constante.
Construir e invertir la tabla de longitudes
function buildArcLengthTable(P, samples = 128) {
const ts = new Float64Array(samples + 1);
const ss = new Float64Array(samples + 1);
for (let i = 1; i <= samples; i++) {
ts[i] = i / samples;
ss[i] = cubicArcLengthTo(ts[i], P);
}
return { ts, ss, length: ss[samples] };
}
function parameterAtLength(table, target, P) {
const { ts, ss } = table;
// Binary search for the pair of samples that bracket the target.
let lo = 0;
let hi = ts.length - 1;
while (lo + 1 < hi) {
const mid = (lo + hi) >> 1;
if (ss[mid] <= target) lo = mid;
else hi = mid;
}
// Linear interpolation gives a good starting point.
const span = ss[hi] - ss[lo];
let t = span > 0 ? ts[lo] + ((target - ss[lo]) / span) * (ts[hi] - ts[lo]) : ts[lo];
// Newton steps refine the answer to full precision.
for (let i = 0; i < 3; i++) {
const speed = cubicSpeed(t, P);
if (speed < 1e-9) break;
t -= (cubicArcLengthTo(t, P) - target) / speed;
t = Math.min(1, Math.max(0, t));
}
return t;
}
La tabla se construye una vez por forma de curva. Cambiar un punto de control la invalida, pero mover un punto a lo largo de la curva en tiempo de ejecución solo realiza consultas. Para mover un objeto a rapidez constante , avanza la distancia en veces el tiempo transcurrido y llama a parameterAtLength con el resultado:
const table = buildArcLengthTable(P);
let travelled = 0;
let previous = performance.now();
function step(now, targetSpeed) {
travelled += targetSpeed * (now - previous) / 1000;
previous = now;
const distance = travelled % table.length;
const t = parameterAtLength(table, distance, P);
// Sample the curve at t to place the object.
}
Sin las consultas a la tabla, avanzar el parámetro directamente haría que el objeto diera tumbos por las partes rápidas de la curva y se arrastrara por las lentas.
Comprobar la implementación
Unas pocas aserciones cubren las ramas principales. La forma cerrada cuadrática debería coincidir con la regla de cuadratura en la misma curva, porque la regla de cuadratura es lo bastante precisa a esta escala. El viaje de ida y vuelta por la tabla debería devolver la distancia original tras convertir a un parámetro y volver.
const q = [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 0 }];
console.assert(Math.abs(quadraticArcLength(q[0], q[1], q[2]) - 3.7546364123) < 1e-9);
// A straight-line quadratic has length equal to its endpoint distance.
console.assert(Math.abs(quadraticArcLength({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }) - Math.SQRT2 * 2) < 1e-9);
const c = [{ x: 0, y: 0 }, { x: 1, y: 3 }, { x: 4, y: -2 }, { x: 5, y: 1 }];
console.assert(Math.abs(cubicArcLength(c) - 6.2147075374) < 1e-5);
const table = buildArcLengthTable(c);
for (let i = 0; i <= 10; i++) {
const target = (i / 10) * table.length;
const t = parameterAtLength(table, target, c);
console.assert(Math.abs(cubicArcLengthTo(t, c) - target) < 1e-6);
}
Casos límite y notas prácticas
Líneas rectas. Cuando un punto de control cuadrático se sitúa exactamente en el punto medio de sus vecinos, la curva colapsa en un segmento recto, y la cuerda entre extremos es la longitud total. La rama cuadrática maneja este caso directamente. Una cúbica cuyos puntos de control son colineales y cuya dirección nunca se invierte también recorre un segmento recto, y la cuadratura devuelve el valor correcto porque la rapidez es constante a lo largo de la línea.
Puntos estacionarios y cúspides. Una curva cúbica puede tener un valor del parámetro en el que su velocidad es cero, lo que ocurre cuando la curva invierte su dirección o forma una cúspide. La función de rapidez toca cero allí y tiene una esquina afilada, y la cuadratura gaussiana pierde precisión. El método de subdivisión adaptativa de arriba es la opción fiable para estas formas.
Autointersecciones. Una cúbica con forma de bucle se cruza a sí misma, pero su longitud de arco sigue contando después del cruce. Eso coincide con la distancia que recorre un punto, que suele ser lo que necesitan la animación y el desplazamiento de curvas. Si en cambio quieres la longitud del contorno visible, divide la curva en el cruce y cuenta cada rama una vez.
Dos dimensiones y tres dimensiones. Las fórmulas solo usan suma de vectores, productos escalares y longitudes, así que el mismo código funciona para puntos de control en 2D y 3D. Un punto de control en 3D simplemente tiene un campo , y la rapidez gana un tercer término al cuadrado.
Precalcula cuando la forma es fija. Una tabla de longitudes para una trayectoria estática puede construirse una vez y almacenarse. Una herramienta que solo lee la longitud total puede omitir la tabla por completo y llamar a la cuadratura, que es un puñado de multiplicaciones y raíces cuadradas.
Inversión y simetría de la curva. Invertir el orden de los puntos de control deja la longitud sin cambios, y lo mismo hacen la forma cerrada y la regla de cuadratura. Es una comprobación de sentido útil cuando una curva se construye desde cualquiera de los dos extremos.
Resumen
La longitud de arco de una curva de Bézier es la integral de la rapidez sobre el rango del parámetro. Para una curva cuadrática, la velocidad es lineal en , así que la rapidez es la raíz cuadrada de un polinomio cuadrático y la integral tiene la forma cerrada derivada arriba. Para una curva cúbica, la rapidez es la raíz cuadrada de un polinomio de cuarto grado, que no tiene primitiva elemental, así que una regla de Gauss-Legendre de 16 puntos calcula la longitud con unos seis dígitos significativos, y la subdivisión adaptativa cubre las curvas con esquinas. Invertir la longitud acumulada da la parametrización por longitud, que una tabla de consulta, una búsqueda binaria y unos pocos pasos de Newton implementan para el movimiento a rapidez constante. El tutorial interactivo de curvas de Bézier cubre la geometría de los puntos de control detrás de estas longitudes.