Bezier Curve Turning Points
2023-08-08
In graphics applications, it is mainly Cubic Bezier curves that are used. Cubic Bezier curves are defined by the following equation:
def bezier(p0, q0, q1, p1):
return lambda t : (1-t)**3 * p0 + 3*(1-t)**2 * t * q0 \
+ 3*(1-t) * t**2 * q1 + t**3 * p1
These are two dimensional curves that go from a point
When trying to code vector shapes such as Bezier curves, computing their bounding rectangle is important, as this allows the graphics computations to efficient understand where to begin evaluating the curve and its coloring from. If it didn't have a boundary rectangle, then each individual pixel on the screen would have to be checked to see it's part of the curve or not. Furthermore, these bounding rectangle computations are also used to determine the selection rectangle for when the curve is selected, or if its bounds are within the bounds of the selection attempted by the cursor.
To compute the bounding rectangle, we find the turning points of the curve. These are the points where the derivative of the curve is zero. The derivative of the cubic bezier is:
where
From this simplification, we have a quadratic formula which will be computed
through element-wise vector operations. That is, products of two vectors will
be defined as
By the quadratic formula, we want that
where
Let's clarify what it is we've done this for. This above derivation will give
us 4 separate equations for
Since the
Not much of a change, besides the fact we're now looking at system of two equations which we've condensed into vector form. Again, keep in mind all arithmetic operations here are just element-wise vector operations on numbers: the subtractions, additions, square roots, products, and divisions.
The code from here is straightforward. We apply the above values and compute
Due to the square roots and the above not simplifying into a nice square, the
computations will only get uglier if we try to simplify further. So we'll just
apply the values for
def turning_points_t(p0, q0, q1, p1):
a = - p0 + 2 * q0 - 3 * q1 + p1
b = p0 - 2 * q0 + q1
c = - p0 + q0
d = sqrt(b * b - a * c)
return ((b - d) / a, (b + d) / a);
Again, as a reminder, the above code gets applied twice: once for the
x-coordinate, and once for the y-coordinate. It however, only provides the
Furthermore, these values must be between 0 and 1. If they are not, then they are not part of the drawn Bezier curve, and are therefore ignored. Thus, we need to compare the following 4 points, and get the minimum and maximum in both x- and y-coordinates:
We can however, ignore if they are below zero or above one. We will work around this as shown:
def bounds(p0, r, s, p1):
# r and s are bezier(t), where t are the turning points
lo = min(p0, r, s, p1)
hi = max(p0, r, s, p1)
return (lo, hi)
And yet again, the above code applies once to the x-coordinate, and once to the y-coordinate.
Putting it all together, we get the following:
def bezier_bounds(p0, q0, q1, q1):
bezier = lambda t: (1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * q0 \
+ 3 * (1 - t) * t ** 2 * q1 + t ** 3 * p1
def turning_points_t(p_0, q_0, q_1, p_1):
a = - p_0 + 2 * q_0 - 3 * q_1 + p_1
b = p_0 - 2 * q_0 + q_1
c = - p_0 + q_0
d = sqrt(b * b - a * c)
return ((b - d) / a, (b + d) / a);
t_x = turning_points_t(p0.x, q0.x, q1.x, p1.x)
t_y = turning_points_t(p0.y, q0.y, q1.y, p1.y)
r_x, s_x = bezier(t_x[0]).x, bezier(t_x[1]).x
r_y, s_y = bezier(t_y[0]).y, bezier(t_y[1]).y
x_min = max(min(p0.x, r_x, s_x, p1.x), min(p0.x, p1.x))
x_max = min(max(p0.x, r_x, s_x, p1.x), max(p0.x, p1.x))
y_min = max(min(p0.y, r_y, s_y, p1.y), min(p0.y, p1.y))
y_max = min(max(p0.y, r_y, s_y, p1.y), max(p0.y, p1.y))
return (x_min, x_max, y_min, y_max)
Our workaround for when