I’m attempting to implement a curve interesection algorithm known as bezier clipping, which is described in a section towards the end of this article (though the article calls it “fat line clipping”). I’ve been following through the article and source code of the example (available here).
Note: Additional sources include this paper. More will be posted if I can find them.
A central part of this algorithm is calculating a “distance function” between curve1 and a “baseline” of curve2 (which is a line from one end point of curve2 to another). So I’d have something to compare my results to, I used the curves from the source code of the first example. I managed to replicate the shape of the distance function from the example, but the distance location of the function was off. Upon trying another curve, the distance function was nowhere near the other two curves, despite both clearly intersecting. I might be naive to the workings of this algorithm, but I think that would result in no intersection being detected.
From what I understand (which could quite possibly be wrong), the process of defining the distance function involves expressing the baseline of curve 2 in the form xa + yb + c = 0, where a2 + b2 = 1. The coefficients were obtained by rearranging the terms of the line in the form y = ux + v, where u is equal to the slope, and x and y are any points on the baseline. The formula can be rearranged to give v: v = y – ux. Rearranging the formula again, we obtain -u*x + 1*y – v = 0, where a = -u, b = 1, and c = -v. To assure the condition a2 + b2 = 1, the coefficients are divided by a scalar of Math.sqrt(uu + 1). This representation of the line is then substituted into the function of the other curve (the one the baseline isn’t associated with) to get the distance function. This distance function is represented as a bezier curve, with yi = aPi x + b*Pi y + c and xi = (1 – t)x1 + tx2, where t is equal to 0, 1/3, 2/3, and 3m x1 and x2 are the endpoints of the baseline, and Pi are the control points of the curve1.
Below are a few cuts of the source code of the example program (written in the language processing) involved with calculating the distance function, which, oddly, uses a slightly different approach to the above paragraph for calculating the alternative representation of the baseline.
/**
* Set up four points, to form a cubic curve, and a static curve that is used for intersection checks
*/
void setupPoints()
{
points = new Point[4];
points[0] = new Point(85,30);
points[1] = new Point(180,50);
points[2] = new Point(30,155);
points[3] = new Point(130,160);
curve = new Bezier3(175,25, 55,40, 140,140, 85,210);
curve.setShowControlPoints(false);
}
...
flcurve = new Bezier3(points[0].getX(), points[0].getY(),
points[1].getX(), points[1].getY(),
points[2].getX(), points[2].getY(),
points[3].getX(), points[3].getY());
...
void drawClipping()
{
double[] bounds = flcurve.getBoundingBox();
// get the distances from C1's baseline to the two other lines
Point p0 = flcurve.points[0];
// offset distances from baseline
double dx = p0.x - bounds[0];
double dy = p0.y - bounds[1];
double d1 = sqrt(dx*dx+dy*dy);
dx = p0.x - bounds[2];
dy = p0.y - bounds[3];
double d2 = sqrt(dx*dx+dy*dy);
...
double a, b, c;
a = dy / dx;
b = -1;
c = -(a * flcurve.points[0].x - flcurve.points[0].y);
// normalize so that a² + b² = 1
double scale = sqrt(a*a+b*b);
a /= scale; b /= scale; c /= scale;
// set up the coefficients for the Bernstein polynomial that
// describes the distance from curve 2 to curve 1's baseline
double[] coeff = new double[4];
for(int i=0; i<4; i++) { coeff[i] = a*curve.points[i].x + b*curve.points[i].y + c; }
double[] vals = new double[4];
for(int i=0; i<4; i++) { vals[i] = computeCubicBaseValue(i*(1/3), coeff[0], coeff[1], coeff[2], coeff[3]); }
translate(0,100);
...
// draw the distance Bezier function
double range = 200;
for(float t = 0; t<1.0; t+=1.0/range) {
double y = computeCubicBaseValue(t, coeff[0], coeff[1], coeff[2], coeff[3]);
params.drawPoint(t*range, y, 0,0,0,255); }
...
translate(0,-100);
}
...
/**
* compute the value for the cubic bezier function at time=t
*/
double computeCubicBaseValue(double t, double a, double b, double c, double d) {
double mt = 1-t;
return mt*mt*mt*a + 3*mt*mt*t*b + 3*mt*t*t*c + t*t*t*d; }
And here is the class (an extension of javax.swing.JPanel) I wrote to recreate the above code:
package bezierclippingdemo2;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
public class ReplicateBezierClippingPanel extends JPanel {
CubicCurveExtended curve1, curve2;
public ReplicateBezierClippingPanel(CubicCurveExtended curve1, CubicCurveExtended curve2) {
this.curve1 = curve1;
this.curve2 = curve2;
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(1));
g2d.setColor(Color.black);
drawCurve1(g2d);
drawCurve2(g2d);
drawDistanceFunction(g2d);
}
public void drawCurve1(Graphics2D g2d) {
double range = 200;
double t = 0;
double prevx = curve1.x1*(1 - t)*(1 - t)*(1 - t) + 3*curve1.ctrlx1*(1 - t)*(1 - t)*t + 3*curve1.ctrlx2*(1 - t)*t*t + curve1.x2*t*t*t;
double prevy = curve1.y1*(1 - t)*(1 - t)*(1 - t) + 3*curve1.ctrly1*(1 - t)*(1 - t)*t + 3*curve1.ctrly2*(1 - t)*t*t + curve1.y2*t*t*t;
for(t += 1.0/range; t < 1.0; t += 1.0/range) {
double x = curve1.x1*(1 - t)*(1 - t)*(1 - t) + 3*curve1.ctrlx1*(1 - t)*(1 - t)*t + 3*curve1.ctrlx2*(1 - t)*t*t + curve1.x2*t*t*t;
double y = curve1.y1*(1 - t)*(1 - t)*(1 - t) + 3*curve1.ctrly1*(1 - t)*(1 - t)*t + 3*curve1.ctrly2*(1 - t)*t*t + curve1.y2*t*t*t;
g2d.draw(new LineExtended(prevx, prevy, x, y));
prevx = x;
prevy = y;
}
}
public void drawCurve2(Graphics2D g2d) {
double range = 200;
double t = 0;
double prevx = curve2.x1*(1 - t)*(1 - t)*(1 - t) + 3*curve2.ctrlx1*(1 - t)*(1 - t)*t + 3*curve2.ctrlx2*(1 - t)*t*t + curve2.x2*t*t*t;
double prevy = curve2.y1*(1 - t)*(1 - t)*(1 - t) + 3*curve2.ctrly1*(1 - t)*(1 - t)*t + 3*curve2.ctrly2*(1 - t)*t*t + curve2.y2*t*t*t;
for(t += 1.0/range; t < 1.0; t += 1.0/range) {
double x = curve2.x1*(1 - t)*(1 - t)*(1 - t) + 3*curve2.ctrlx1*(1 - t)*(1 - t)*t + 3*curve2.ctrlx2*(1 - t)*t*t + curve2.x2*t*t*t;
double y = curve2.y1*(1 - t)*(1 - t)*(1 - t) + 3*curve2.ctrly1*(1 - t)*(1 - t)*t + 3*curve2.ctrly2*(1 - t)*t*t + curve2.y2*t*t*t;
g2d.draw(new LineExtended(prevx, prevy, x, y));
prevx = x;
prevy = y;
}
}
public void drawDistanceFunction(Graphics2D g2d) {
double a = (curve1.y2 - curve1.y1)/(curve1.x2 - curve1.x1);
double b = -1;
double c = -(a*curve1.x1 - curve1.y1);
double scale = Math.sqrt(a*a + b*b);
a /= scale;
b /= scale;
c /= scale;
double y1 = a*curve2.x1 + b*curve2.y1 + c;
double y2 = a*curve2.ctrlx1 + b*curve2.ctrly1 + c;
double y3 = a*curve2.ctrlx1 + b*curve2.ctrly2 + c;
double y4 = a*curve2.x2 + b*curve2.y2 + c;
double range = 200;
double t = 0;
double prevx = t*range;
double prevy = (1 - t)*(1 - t)*(1 - t)*y1 + 3*(1 - t)*(1 - t)*t*y2 + 3*(1 - t)*t*t*y3 + t*t*t*y4;
for(t += 1.0/range; t < 1.0; t += 1.0/range) {
double x = t*range;
double y = (1 - t)*(1 - t)*(1 - t)*y1 + 3*(1 - t)*(1 - t)*t*y2 + 3*(1 - t)*t*t*y3 + t*t*t*y4;
g2d.draw(new LineExtended(prevx, prevy, x, y));
prevx = x;
prevy = y;
}
}
}
Where CubicCurveExtended and LineExtended are minor extensions of java.awt.geom.CubicCurve2D.Double and java.awt.geom.Line2D.Double. Before the curves are passed into the constructor, the curves are rotated uniformly so curve1’s endpoints are level, resulting in a slope of zero for the baseline.
For an input of (485, 430, 580, 60, 430, 115, 530, 160) for curve 1 and (575, 25, 455, 60, 541, 140, 486, 210) for curve2 (keep in mind that these values are rotated by the negative angle between the endpoints of curve1), the result is shown below (the distance function is the relatively smooth looking curve off in the distance):

I’m really not sure what I got wrong. The y values seem to be arranged in the right pattern, but are distant from the two curves it’s based on. I realize it’s possible I have the x values might be arranged at intervals along the curve rather than the baseline, but the y values are what I’m really confused about. If someone can take a look at this and explain what I got wrong, I’d really appreciate it. Thanks for taking the time to read this rather lengthy question. If more details are needed, feel free to tell me in comments.
Update: I’ve tested the representation of the line I’ve calculated. The ax + by + c = 0 representation apparently still represents the same line, as I can still plug in x1 and get y1. Additionally, for any two coordinate pairs plugged into the function, f(x, y) = 0 holds. Furthermore, I’ve found both the representation described in the article and the one actually used in the source code interchangeably represent the same line. From all this, I can assume the problem doesn’t lie in calculating the line. An additional point of interest
Your distance function should not necessarily be anywhere near your two original curves: It’s using a completely different coordinate system, i.e. t vs D, as opposed to your original curves using x and y. [edit] i.e. t only goes up to 1.0, and measures how far along, as a ratio of the total length, you are along your curve, and D measuring the distance your curve2 is from curve1’s baseline.
Also, when you say “”distance function” between curve1 and a “baseline” of curve2″ I think you’ve mixed up curve1 and curve2 here as in your code you are clearly using the baseline of curve1.
Also the algorithm assumes that “every Bézier curve is fully contained by the polygon that connects all the start/control/end points, known as its “convex hull”” which [edit] in your case for curve1 is a triangle, where the control point for the second starting value is not a vertex. I’m not sure how this affects the algorithm though.
Otherwise, it looks like your distance calculations are fine (although you could really do with optimising things a bit 🙂 ).