De booglengte van een Bezier-kromme is de totale afstand die een punt aflegt van het begin tot het einde van de kromme. Je krijgt die door de snelheid van dat punt te integreren, de lengte van de afgeleide van de kromme, over het parameterbereik. Een kwadratische Bezier-kromme heeft een exacte gesloten vorm. Een kubische Bezier-kromme heeft die over het algemeen niet, dus benaderen implementaties de lengte met numerieke kwadratuur. Deze pagina leidt de kwadratische formule af, bouwt de kubische benadering op en laat zien hoe je booglengte omzet in een lengteparametrisatie die een punt met constante snelheid voortbeweegt. De interactieve tutorial over Bezier-krommen behandelt de meetkunde van de controlepunten waarop alles hier voortbouwt.
Booglengte is de integraal van de snelheid
Een Bezier-kromme kent aan elke parameterwaarde tussen en een punt toe. Naarmate toeneemt, schuift dat punt langs de kromme. De snelheidsvector is de afgeleide , een vector die in de bewegingsrichting wijst, en de snelheid is de lengte van die vector:
De snelheid meet de afgelegde afstand per eenheid van . Snelheid maal een infinitesimale stap geeft de afstand die tijdens die stap wordt afgelegd, dus door al die kleine stukjes op te tellen krijg je het totaal:
Hier is de parameter van de kromme, een dummy-variabele van de integratie, de booglengte die vanaf het begin tot parameter is opgebouwd, en de volledige lengte van de kromme. De rechte lijn tussen de eindpunten is altijd korter dan de kromme, en de controleveelhoek, die de controlepunten met rechte segmenten verbindt, is altijd langer. De werkelijke lengte ligt tussen die twee grenzen en is gelijk aan de oppervlakte onder de snelheidskromme uitgezet tegen .
De belangrijke praktische consequentie is dat geen afstand is. Gelijke stappen in leveren ongelijke stappen in afstand op wanneer de snelheid langs de kromme verandert. Een renderer die gelijkmatig in samplet, plaatst meer punten waar de kromme langzaam is en minder waar ze snel is.
Het linkerpaneel is een bewerkbare kwadratische of kubische kromme. Het rechterpaneel zet de bijbehorende snelheid uit tegen en arceert de oppervlakte eronder. Die gearceerde oppervlakte is de booglengte die in de hoek wordt weergegeven. Een controlepunt slepen verandert beide panelen tegelijk: de controlepunten uit elkaar trekken verhoogt de snelheidskromme en laat de oppervlakte groeien, terwijl een compacte vorm de kromme verlaagt en laat krimpen. Schakel over naar het kubische geval en de snelheidskromme kan meerdere bulten ontwikkelen, omdat de afgeleide van een kubische kromme een kwadratische polynoom is waarvan de lengte vrijer verandert dan de lineaire afgeleide van een kwadratische kromme.
De volledige lengte van een kwadratische Bezier-kromme vinden
Een kwadratische Bezier-kromme gebruikt drie punten en de formule
Differentiëren geeft de snelheidsvector:
Die uitdrukking is lineair in , wat de sleutel tot de gesloten vorm is. Verzamel het constante en het lineaire deel door twee nieuwe vectoren te definiëren:
Nu is de snelheidsvector eenvoudigweg
Je kunt dit controleren door uit te werken: het is gelijk aan , de afgeleide hierboven. Omdat de afgeleide een rechte lijn in is, is het kwadraat van de lengte een kwadratische polynoom:
met de drie scalaire coëfficiënten
De snelheid is de vierkantswortel van die kwadratische uitdrukking, en de lengte is de integraal ervan:
Om die te berekenen, maak je het kwadraat af binnen de wortel. Schrijf en . Dan geldt
Substitutie van brengt de integraal in standaardvorm:
De primitieve van voor is
dus de exacte kwadratische booglengte is
De grootheid is nooit negatief voor een kwadratische Bezier-kromme. Het product kan volgens de ongelijkheid van Cauchy-Schwarz nooit groter zijn dan , en dat is precies de voorwaarde waaronder . Wanneer en evenwijdig zijn, is nul, wordt de logaritme singulier en vereenvoudigt de wortel tot . De integraal van de absolute waarde is eenvoudig stuksgewijs te berekenen.
Uitgewerkt voorbeeld
Neem , en . Dan
De coëfficiënten zijn , en . De verschuiving is
en de offset is
Het evalueren van op de twee eindpunten geeft en . Met is de lengte
Kwadratische booglengte in code
Deze functie geeft de exacte lengte terug, inclusief de gevallen van een rechte lijn en een evenwijdige snelheidsvector:
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));
}
De tak a === 0 treedt op wanneer het midden is van en , waardoor de kromme tot een recht lijnstuk samenvouwt. De tak met bijna-nul discriminant vangt de situatie op waarin de snelheidsvector op één lijn blijft maar van richting kan omkeren, zodat de kromme heen en terug beweegt. Voor het gangbare geval draait de gesloten vorm in constante tijd zonder sampling.
Waarom de booglengte van een kubische Bezier geen gesloten vorm heeft
Een kubische Bezier-kromme gebruikt vier punten:
De afgeleide ervan is
Elke term is een vector maal een kwadratische polynoom in , dus is een vector van graad twee en is één enkele vierdegraadspolynoom in . De snelheid is de vierkantswortel van die vierdegraadspolynoom. Integralen van de vierkantswortel van een algemene vierdegraadspolynoom zijn elliptische integralen, en elliptische integralen hebben geen uitdrukking in elementaire functies. Er bestaat geen logaritmische of wortelformule die elke kubische kromme dekt zoals de kwadraatafsplitsingsformule elke kwadratische kromme dekt.
Dat betekent niet dat de lengte onkenbaar is. Het betekent dat de lengte moet worden berekend in plaats van opgezocht, en het standaardgereedschap daarvoor is Gauss-kwadratuur.
De booglengte van een kubische Bezier benaderen
Gauss-Legendre-kwadratuur benadert een bepaalde integraal door de integrand op een klein aantal punten te sampelen en een gewogen som te nemen:
De punten zijn de wortels van de -de Legendre-polynoom, en de gewichten zijn zo gekozen dat de regel exact is voor elke polynoom tot graad . Ons interval is in plaats van , dus substitueren we , wat geeft
Zestien punten maken de regel exact voor polynomen tot graad 31. Snelheid is een vierkantswortel en geen polynoom, dus de regel is niet exact, maar voor een gladde snelheidsfunctie komen 16 punten al binnen enkele delen op een miljoen van de werkelijke lengte uit.
// 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;
}
Dezelfde hulpfunctie werkt wanneer de kwadratuur de lengte van het begin tot een willekeurige parameter nodig heeft in plaats van de hele kromme. Behoud de abscissen en gewichten en schaal het interval naar :
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;
}
Hoe nauwkeurig is de kwadratuur?
De tabel vergelijkt de regel met een referentie met hoge precisie voor drie kubische krommen. De fouten zijn absoluut.
| Controlepunten van de kubische kromme | Werkelijke lengte | 8 punten | 16 punten | 24 punten |
|---|---|---|---|---|
| 6.214707537 | ||||
| 3.443380724 | ||||
| 1.154700538 |
De eerste twee krommen hebben gladde snelheidsprofielen, en 16 punten zijn ruim voldoende. De derde kromme keert op zichzelf terug, waardoor haar snelheid tot nul daalt en de snelheidsfunctie een hoek krijgt. Kwadratuur convergeert langzaam bij een hoek, en meer punten toevoegen helpt nauwelijks. De oplossing is om het parameterbereik op het lastige punt te splitsen en elk stuk apart te integreren, of om de onderverdelingsmethode hieronder te gebruiken.
Adaptieve onderverdeling als alternatief
Het algoritme van De Casteljau kan een kubisch segment bij elke parameterwaarde in twee kleinere kubische segmenten splitsen, en splitsen bij is bijzonder eenvoudig: middel naburige controlepunten totdat één punt overblijft. De controleveelhoek van een klein stukje is bijna een rechte lijn, dus zowel de lengte ervan als de koordelengte tussen de eindpunten liggen dicht bij de werkelijke booglengte. Het verschil ertussen meet hoe gebogen het stukje nog is. Wanneer het verschil onder een tolerantie ligt, geef je het gemiddelde van de twee terug; anders splits je en ga je recursief verder.
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)
);
}
Adaptieve onderverdeling kost meer evaluaties dan één enkele 16-puntsregel, maar concentreert het werk waar de kromme buigt en blijft werken bij krommen met hoeken of bijna-cuspen. De kwadratuurregel is sneller voor de gladde krommen die in de meeste rendering- en animatiewerk voorkomen, en onderverdeling is de veilige terugvaloptie wanneer een tolerantie gegarandeerd moet worden.
Lengteparametrisatie van een Bezier-kromme
Alles tot nu toe berekent de lengte uit de parameter. De omgekeerde vraag, het vinden van de parameter die bij een gegeven afstand hoort, is wat beweging met constante snelheid mogelijk maakt. Definieer de opgebouwde-lengtefunctie
Omdat snelheid nooit negatief is, stijgt gelijkmatig van naar terwijl van naar gaat, dus de functie kan worden geïnverteerd. De inverse, geschreven als , is de lengteparametrisatie: geef er een afstand aan en ze geeft de parameter terug die het punt precies zo ver langs de kromme plaatst. De voorwaartse richting heeft een gesloten vorm voor kwadratische krommen en een kwadratuurregel voor kubische krommen, maar geen van beide inversen heeft in het algemeen een elementaire uitdrukking, dus bouwen implementaties een tabel op en interpoleren ze.
Het recept bestaat uit drie stappen. Sample op veel gelijkmatig verdeelde -waarden en sla de paren op. Om een doelfstand naar een parameter om te zetten, zoek je in de tabel binair naar het omringende paar en interpoleer je lineair. Omdat de tabel slechts een stuksgewijs lineaire benadering van een gladde functie is, sluit je af met een of twee Newton-stappen. De methode van Newton vindt een nulpunt van met de afgeleide , precies de snelheid die al beschikbaar is.
Beide panelen tonen dezelfde kubische kromme. Het linkerpaneel plaatst een stip bij elke tiende van het parameterbereik, en het rechterpaneel plaatst een stip bij elke tiende van de totale booglengte. Een controlepunt slepen hervormt beide panelen samen. Waar de kromme langzaam beweegt, kruipen de parametermarkeringen samen, en waar ze snel beweegt, spreiden ze uiteen. De booglengtemarkeringen blijven gelijkmatig verdeeld omdat de tabel elke gelijke afstand terugvertaalt naar de parameter die die afstand bereikt. De rode markeringen doorlopen beide krommen in dezelfde tijd. De linker markering versnelt en vertraagt met de parameter, en de rechter markering beweegt in een gelijkmatig tempo.
De lengtetabel opbouwen en inverteren
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;
}
De tabel wordt één keer per krommevorm opgebouwd. Het wijzigen van een controlepunt maakt de tabel ongeldig, maar een punt langs de kromme verplaatsen tijdens runtime voert alleen opzoekingen uit. Om een object met constante snelheid te verplaatsen, verhoog je de afstand met maal de verstreken tijd en roep je parameterAtLength aan met het resultaat:
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.
}
Zonder de tabelopzoekingen zou het direct verhogen van de parameter het object door de snelle delen van de kromme laten schokken en door de langzame delen laten kruipen.
De implementatie controleren
Een paar asserties dekken de belangrijkste takken. De kwadratische gesloten vorm zou moeten overeenkomen met de kwadratuurregel op dezelfde kromme, omdat de kwadratuurregel op deze schaal nauwkeurig genoeg is. De heen-en-terugreis door de tabel zou de oorspronkelijke afstand moeten teruggeven nadat je naar een parameter en terug hebt omgezet.
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);
}
Randgevallen en praktische opmerkingen
Rechte lijnen. Wanneer een kwadratisch controlepunt precies op het midden van zijn buren ligt, valt de kromme samen met een recht lijnstuk, en is de koorde tussen de eindpunten de volledige lengte. De kwadratische tak behandelt dit geval rechtstreeks. Een kubische kromme waarvan de controlepunten collineair zijn en waarvan de richting nooit omkeert, doorloopt ook een recht lijnstuk, en de kwadratuur geeft de juiste waarde terug omdat de snelheid langs de lijn constant is.
Stationaire punten en cuspen. Een kubische kromme kan een parameterwaarde hebben waarop haar snelheidsvector nul is, wat gebeurt wanneer de kromme van richting verandert of een cusp vormt. De snelheidsfunctie raakt daar nul en heeft een scherpe hoek, en Gauss-kwadratuur verliest nauwkeurigheid. De adaptieve onderverdelingsmethode hierboven is de betrouwbare keuze voor deze vormen.
Zelfsnijdingen. Een lusvormige kubische kromme snijdt zichzelf, maar haar booglengte blijft na de kruising doorlopen. Dat komt overeen met de afstand die een punt aflegt, wat meestal is wat animatie en offsetting nodig hebben. Wil je in plaats daarvan de lengte van de zichtbare omtrek, splits de kromme dan op de kruising en tel elke tak één keer mee.
Twee en drie dimensies. De formules gebruiken alleen vectoroptelling, inwendige producten en lengtes, dus dezelfde code werkt voor 2D- en 3D-controlepunten. Een 3D-controlepunt heeft eenvoudigweg een -veld, en de snelheid krijgt er een derde kwadraatterm bij.
Vooraf berekenen wanneer de vorm vastligt. Een lengtetabel voor een statisch pad kan één keer worden opgebouwd en opgeslagen. Een hulpmiddel dat alleen de totale lengte leest, kan de tabel helemaal overslaan en de kwadratuur aanroepen, wat neerkomt op een handvol vermenigvuldigingen en vierkantswortels.
Omkering en symmetrie van de kromme. Het omkeren van de volgorde van de controlepunten laat de lengte onveranderd, en dat geldt ook voor de gesloten vorm en de kwadratuurregel. Dit is een nuttige sanity check wanneer een kromme vanaf beide uiteinden wordt opgebouwd.
Samenvatting
De booglengte van een Bezier-kromme is de integraal van de snelheid over het parameterbereik. Voor een kwadratische kromme is de snelheidsvector lineair in , dus de snelheid is de vierkantswortel van een kwadratische polynoom en heeft de integraal de gesloten vorm die hierboven is afgeleid. Voor een kubische kromme is de snelheid de vierkantswortel van een vierdegraadspolynoom, die geen elementaire primitieve heeft, dus berekent een 16-punts Gauss-Legendre-regel de lengte tot ongeveer zes significante cijfers, en dekt adaptieve onderverdeling krommen met hoeken. Het inverteren van de opgebouwde lengte geeft de lengteparametrisatie, die een opzoektabel, binair zoeken en een paar Newton-stappen implementeren voor beweging met constante snelheid. De interactieve tutorial over Bezier-krommen behandelt de meetkunde van de controlepunten achter deze lengtes.