Find the Tangent of a Point on a Cubic Bezier Curve

Find the tangent of a point on a cubic bezier curve

Here is fully tested code to copy and paste:

It draws approxidistant points along the curve, and it draws the tangents.

bezierInterpolation finds the points

bezierTangent finds the tangents

There are TWO VERSIONS of bezierInterpolation supplied below:

bezierInterpolation works perfectly.

altBezierInterpolation is exactly the same, BUT it is written in an expanded, clear, explanatory manner. It makes the arithmetic much easier to understand.

Use either of those two routines: the results are identical.

In both cases, use bezierTangent to find the tangents. (Note: Michal's fabulous code base here.)

A full example of how to use with drawRect: is also included.

// MBBezierView.m    original BY MICHAL stackoverflow #4058979

#import "MBBezierView.h"



CGFloat bezierInterpolation(
CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
// see also below for another way to do this, that follows the 'coefficients'
// idea, and is a little clearer
CGFloat t2 = t * t;
CGFloat t3 = t2 * t;
return a + (-a * 3 + t * (3 * a - a * t)) * t
+ (3 * b + t * (-6 * b + b * 3 * t)) * t
+ (c * 3 - c * 3 * t) * t2
+ d * t3;
}

CGFloat altBezierInterpolation(
CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
// here's an alternative to Michal's bezierInterpolation above.
// the result is absolutely identical.
// of course, you could calculate the four 'coefficients' only once for
// both this and the slope calculation, if desired.
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );

// it's now easy to calculate the point, using those coefficients:
return ( C1*t*t*t + C2*t*t + C3*t + C4 );
}







CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
// note that abcd are aka x0 x1 x2 x3

/* the four coefficients ..
A = x3 - 3 * x2 + 3 * x1 - x0
B = 3 * x2 - 6 * x1 + 3 * x0
C = 3 * x1 - 3 * x0
D = x0

and then...
Vx = 3At2 + 2Bt + C */

// first calcuate what are usually know as the coeffients,
// they are trivial based on the four control points:

CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a ); // (not needed for this calculation)

// finally it is easy to calculate the slope element,
// using those coefficients:

return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );

// note that this routine works for both the x and y side;
// simply run this routine twice, once for x once for y
// note that there are sometimes said to be 8 (not 4) coefficients,
// these are simply the four for x and four for y,
// calculated as above in each case.
}







@implementation MBBezierView

- (void)drawRect:(CGRect)rect {
CGPoint p1, p2, p3, p4;

p1 = CGPointMake(30, rect.size.height * 0.33);
p2 = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
p3 = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
p4 = CGPointMake(-30 + CGRectGetMaxX(rect), rect.size.height * 0.66);

[[UIColor blackColor] set];
[[UIBezierPath bezierPathWithRect:rect] fill];
[[UIColor redColor] setStroke];
UIBezierPath *bezierPath = [[[UIBezierPath alloc] init] autorelease];
[bezierPath moveToPoint:p1];
[bezierPath addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
[bezierPath stroke];

[[UIColor brownColor] setStroke];

// now mark in points along the bezier!

for (CGFloat t = 0.0; t <= 1.00001; t += 0.05) {
[[UIColor brownColor] setStroke];

CGPoint point = CGPointMake(
bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x),
bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));

// there, use either bezierInterpolation or altBezierInterpolation,
// identical results for the position

// just draw that point to indicate it...
UIBezierPath *pointPath =
[UIBezierPath bezierPathWithArcCenter:point
radius:5 startAngle:0 endAngle:2*M_PI clockwise:YES];
[pointPath stroke];

// now find the tangent if someone on stackoverflow knows how
CGPoint vel = CGPointMake(
bezierTangent(t, p1.x, p2.x, p3.x, p4.x),
bezierTangent(t, p1.y, p2.y, p3.y, p4.y));

// the following code simply draws an indication of the tangent
CGPoint demo = CGPointMake( point.x + (vel.x*0.3),
point.y + (vel.y*0.33) );
// (the only reason for the .3 is to make the pointers shorter)
[[UIColor whiteColor] setStroke];
UIBezierPath *vp = [UIBezierPath bezierPath];
[vp moveToPoint:point];
[vp addLineToPoint:demo];
[vp stroke];
}
}

@end

to draw that class...
MBBezierView *mm = [[MBBezierView alloc]
initWithFrame:CGRectMake(400,20, 600,700)];
[mm setNeedsDisplay];
[self addSubview:mm];

Here are the two routines to calculate approximately equidistant points, and the tangents of those, along a bezier cubic.

For clarity and reliability, these routines are written in the simplest, most explanatory, way possible.

CGFloat bezierPoint(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );

return ( C1*t*t*t + C2*t*t + C3*t + C4 );
}

CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
{
CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
CGFloat C4 = ( a );

return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
}

The four precalculated values, C1 C2 C3 C4, are sometimes called the coefficients of the bezier. (Recall that a b c d are usually called the four control points.)

Of course, t runs from 0 to 1, for example every 0.05.

Simply call these routines once for X, and then once separately for Y.

Hope it helps someone!


Important facts:

(1) It is an absolute fact that: unfortunately, there is, definitely, NO method, provided by Apple, to extract points from a UIBezierPath. True as of 2019.

(2) Don't forget it's as easy as pie to animate something along a UIBezierPath. Google many examples.

(3) Many ask, "Can't CGPathApply be used to extract the points from a UIBezierPath?" No, CGPathApply is totally unrelated: it simply gives you a list of your "instructions in making any path" (so, "start here", "draw a straight line to this point", etc etc.) The name is confusing but CGPathApply is totally unrelated to bezier paths.


For game programmers - as @Engineer points out you may well want the normal of the tangent, fortunately Apple has vector math built-in:

https://developer.apple.com/documentation/accelerate/simd/working_with_vectors

https://developer.apple.com/documentation/simd/2896658-simd_normalize

Calculate the slope of a tangent of an arbitrary point on a quadratic curve in Javascript

This will return the normalised tangent at the unit position for a 2nd and 3rd order curve.

See this answer for more detailed usage of the object below.

const geom = (()=>{

function Vec(x,y){
this.x = x;
this.y = y;
};
function Bezier(p1,p2,cp1,cp2){
this.p1 = p1;
this.p2 = p2;
this.cp1 = cp1;
this.cp2 = cp2;
}
Bezier.prototype = {
//======================================================================================
// single dimension polynomials for 2nd (a,b,c) and 3rd (a,b,c,d) order bezier
//======================================================================================
// for quadratic f(t) = a(1-t)^2+2b(1-t)t+ct^2
// = a+2(-a+b)t+(a-2b+c)t^2
// The derivative f'(t) = 2(1-t)(b-a)+2(c-b)t
//======================================================================================
// for cubic f(t) = a(1-t)^3 + 3bt(1-t)^2 + 3c(1-t)t^2 + dt^3
// = a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3
// The derivative f'(t) = -3a(1-t)^2+b(3(1-t)^2-6(1-t)t)+c(6(1-t)t-3t^2) +3dt^2
// The 2nd derivative f"(t) = 6(1-t)(c-2b+a)+6t(d-2c+b)
//======================================================================================
p1 : undefined,
p2 : undefined,
cp1 : undefined,
cp2 : undefined,
tangentAsVec (position, vec ) {
var a, b, c, u;
if (vec === undefined) { vec = new Vec(); }

if (this.cp2 === undefined) {
a = (1-position) * 2;
b = position * 2;
vec.x = a * (this.cp1.x - this.p1.x) + b * (this.p2.x - this.cp1.x);
vec.y = a * (this.cp1.y - this.p1.y) + b * (this.p2.y - this.cp1.y);
}else{
a = (1-position)
b = 6 * a * position; // (6*(1-t)*t)
a *= 3 * a; // 3 * ( 1 - t) ^ 2
c = 3 * position * position; // 3 * t ^ 2
vec.x = -this.p1.x * a + this.cp1.x * (a - b) + this.cp2.x * (b - c) + this.p2.x * c;
vec.y = -this.p1.y * a + this.cp1.y * (a - b) + this.cp2.y * (b - c) + this.p2.y * c;
}
u = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= u;
vec.y /= u;
return vec;
},
}
return { Vec, Bezier,}
})()

Usage for 2nd order bezier

   // ? represents any value 
var p1 = new geom.Vec(?,?); // start point
var p2 = new geom.Vec(?,?); // end point
var cp1 = new geom.Vec(?,?); //control point

var bez2 = new geom.Bezier(p1,p2,cp1); // create 2nd order bezier
var t = ?;
var tangent = bez2.tangentAsVec(t);

Usage for 3rd order bezier

   // ? represents any value 
var p1 = new geom.Vec(?,?); // start point
var p2 = new geom.Vec(?,?); // end point
var cp1 = new geom.Vec(?,?); // 1st control point
var cp2 = new geom.Vec(?,?); // 2nd control point

var bez3 = new geom.Bezier(p1,p2,cp1,cp2); // create 3rd order bezier
var t = ?;
var tangent = bez2.tangentAsVec(t);

Find a point, a given distance, along a simple cubic bezier curve. (On an iPhone!)

There's some simple math behind calculating the positions, you can read about it in every paper discussing Bézier curves, even on wikipedia. Anyway, I can relate to everybody who's in trouble to actually implement it in code, so I wrote this sample UIView as it's probably the easiest way to get you started.

#import "MBBezierView.h"

CGFloat bezierInterpolation(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
CGFloat t2 = t * t;
CGFloat t3 = t2 * t;
return a + (-a * 3 + t * (3 * a - a * t)) * t
+ (3 * b + t * (-6 * b + b * 3 * t)) * t
+ (c * 3 - c * 3 * t) * t2
+ d * t3;
}

@implementation MBBezierView

- (void)drawRect:(CGRect)rect {
CGPoint p1, p2, p3, p4;
p1 = CGPointMake(30, rect.size.height * 0.33);
p2 = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
p3 = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
p4 = CGPointMake(-30 + CGRectGetMaxX(rect), rect.size.height * 0.66);

[[UIColor blackColor] set];
[[UIBezierPath bezierPathWithRect:rect] fill];

[[UIColor redColor] setStroke];

UIBezierPath *bezierPath = [[[UIBezierPath alloc] init] autorelease];
[bezierPath moveToPoint:p1];
[bezierPath addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
[bezierPath stroke];

[[UIColor brownColor] setStroke];
for (CGFloat t = 0.0; t <= 1.00001; t += 0.05) {
CGPoint point = CGPointMake(bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x), bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));
UIBezierPath *pointPath = [UIBezierPath bezierPathWithArcCenter:point radius:5 startAngle:0 endAngle:2*M_PI clockwise:YES];
[pointPath stroke];
}
}

@end

This is what I get:

alt text

Calculate cubic bezier T value where tangent is perpendicular to anchor line

Let's assume you got a 1D cubic Bézier curve with P0 = 0 and P3 = 1 then the curve is:

P(t) = b0,3(t)*0 + b1,3(t)*P1 + b2,3(t)*P2 + b3,3(t)*1

Where bi,3(t) are the Bernstein polynomials of degree 3. Then we're looking for the value of t where this P(t) is minimal and maximal, so we derive:

P'(t) = b1,3'(t)*P1 + b2,3'(t)*P2 + b3,3'(t)
= (3 - 12t + 9t^2)*P1 + (6t - 9t^2)*P2 + 3t^2
= 0

This has a closed-form but nontrivial solution. According to WolframAlpha, when 3P1 - 3P2 +1 != 0 it's:

t = [2*P1 - P2 +/- sqrt(P1^2-P1*P2-P1+P2^2)] / (3*P1 - 3*P2 + 1)

Otherwise it's:

t = 3P1 / (6P1 - 2)

For a general n-dimensional cubic Bézier P0*, P1*, P2*, P3* compute:

P1 = proj(P1*, P03*) / |P3* - P0*|
P2 = proj(P2*, P03*) / |P3* - P0*|

Where proj(P, P03*) is the signed distance from P0* to the point P projected on the line passing through P0* and P3*.

(I haven't checked this, so please confirm there is nothing wrong in my reasoning.)

Draw tangents and normal to each point in Bezier curve opengl

Though P'(t) is sometimes called the tangent, it is actually the derivative of the curve, aka the velocity. If the curve is in the 2d space and its points are measured in meters, say, then the units of P'(t) will be in meters/second. It does not make sense to draw a line between '5 meters' and '6 meters/second' because they are points in different spaces.

What you should do is to draw a line between 'the point on the curve' and 'where the object would be if it was detached from the curve and continued to move at its current velocity for 1 second'. That is between P(t) and P(t) + dt * P'(t).

Scaling tangent lines in a bezier curve

The velocity at any point B(t) is the derivative vector B'(t) so you got that part right, but the derivative (remember not to call it the tangent: tangents are true lines without a start or end point) at a point tells you the velocity "over one unit of time". And inconveniently, an entire Bezier curve only covers a single unit of time (with the t parameter running from 0 to 1)...

So, if we space our t values using some smaller-than-one value, like your curve does by using 20 points (so a t interval of 1/20), we need to scale all those derivatives by 1/(point count) too.

If we look at the unscaled derivatives, they are unwieldy and huge:

Quadratic Bezier curve (0,0),(7,0),(1,4) sampled over 20 points, with derivative vectors

But if we scale them by 1/20 so that they represent the velocity vector for the time interval we're actually using, things look really good:

the same curve with scaled derivative vectors

And we see an error between "where the true next point is" and "where the previous point plus its velocity vector says it'll be" based on curvature: the stronger the curvature, the further away from the "true point" a "previous point + velocity over time interval" will be, which we can mitigate by using more points.

If we use only 8 steps in our curve, with the derivatives scaled by 1/8, things don't look all that great:

the same curve, sampled over 8 points

If we bump that up to 15, with 1/15 scaling, things start to look better:

the same curve, sampled over 15 points

And while your curve with 20 points looks alright, let's look at what 50 points with 1/50 scaling gets us:

the same curve, sampled over 50 points

That's preeeetty good.

How to find a point (if any) on quadratic Bezier with a given tangent direction?

Using the notation given in http://pomax.github.io/bezierinfo/#extremities, a quadratic Bezier curve is given by:

B(t) = P1*(1-t)**2 + 2*P2*(1-t)*t + P3*t**2

Therefore, (by taking the derivative of B with respect to t) the tangent to the curve is given by

B'(t) = -2*P1*(1-t) + 2*P2*(1-2*t) + 2*P3*t
= 2*(P1 - 2*P2 + P3)*t + 2*(-P1 + P2)

Given some tangent direction V, solve

B'(t) = V

for t. If there is a solution, t = ts, then the point on the Bezier curve which has tangent direction V is given by B(ts).


We essentially want to know if two vectors (B'(t) and V) are parallel or anti-parallel. There is a trick to doing that.

Two vectors, X and Y, are perpendicular if their dot product is zero. If X = (a,b) and Y = (c,d) then the dot product of X and Y is given by

a*c + b*d

So, X and Y are parallel if X and Y_perp are perpendicular, where Y_perp is a vector perpendicular to Y.

In 2-dimensions, if in coordinates Y = (a,b) then Y_perp = (-b, a). (There are two perpendicular vectors possible, but this one will do.) Notice that -- using the formula above -- the dot product of Y and Y_perp is

a*(-b) + b*(a) = 0

So indeed, this jibes with the claim that perpendicular vectors have a dot product equal to 0.

Now, to solve our problem: Let

B'(t) = (a*t+b, c*t+d)
V = (e, f)

Then B'(t) is parallel (or anti-parallel) to V if or when
B'(t) is perpendicular to V_perp, which occurs when

dot product((a*t+b, c*t+d), (-f, e)) = 0
-(a*t+b)*f + (c*t+d)*e = 0

We know a, b, c, d, e and f. Solve for t. If t lies between 0 and 1, then B(t) is part of the Bezier curve segment between P1 and P3.

Quadratic Bezier Curve: Calculate Tangent

Using your formula for B'(t), evaluated at t=1/2, we get

B'(1/2) = -P0 + P2

From the look of your graph, P0 = (0,0) and P2 = (400,0). So

B'(1/2) = (400,0).

This is the "velocity" of a point traveling along B(t) at t=1/2.

(400,0) is a horizontal vector, with magnitude 400.

So all is as it should be. Since B'(t) is horizontal, it does have "slope" 0.

c++ Bezier curve Tangent direction issues

Since your Bezier curve is on the (x, z) plane, I don't quite understand why do you want to take a cross product between first derivative and the z axis (0, 0, 1). The 'normal' vector found in this way is bound to become zero when the first derivative goes in the (0, 0, 1) direction.

If you want to find the normal direction at any given point on the curve, you should evaluate the first derivative C'(t) and the 2nd derivative C"(t) first, then the normal vector vec(n) can be computed as

vec(n) = C' X C" / |C'||C"|

and your binormal vector vec(b) can be computed as vec(b) = vec(t) X vec(n) where vec(t) is the unit tangent vector vec(t)=C'(t)/|C'(t)|.



Related Topics



Leave a reply



Submit