We can already draw a circle by measuring distance from a point. A rounded line needs a similar measurement, but its center can be anywhere along a segment. For each pixel, we must choose the closest center first.
The dot product from chapter 06 provides the first step. It measures how far a displacement travels along a direction. Now we will turn that number back into a displacement.
Turn the number into a vector
Start with these endpoints and a probe point in our equal-unit coordinate space:
vec2 a = vec2(-0.6, -0.4);
vec2 b = vec2( 0.6, 0.5);
vec2 p = vec2( 0.0, 0.8);
vec2 ab = b - a; // (1.2, 0.9), length 1.5
vec2 u = ab / length(ab); // (0.8, 0.6), for this nonzero ab
The displacement from A to the probe is p - a = (0.6, 1.2). Its scalar projection onto the unit direction is:
float along = dot(p - a, u);
// 0.6 * 0.8 + 1.2 * 0.6 = 1.2
That is one signed distance, in coordinate units. Multiply it by the unit direction:
vec2 projected = along * u; // (0.96, 0.72)
This is the vector projection: a displacement along the line, starting from A. It differs from the scalar projection, which is only the number 1.2. Neither one is yet a point in the scene.
Find the point on the line
Add the projected displacement to its starting point:
vec2 closest = a + projected; // (0.36, 0.32)
vec2 residual = p - closest; // (-0.36, 0.48)
float distanceToLine = length(residual); // 0.6
The residual runs from the closest point to the probe. Its dot product with u is -0.288 + 0.288 = 0, so it is perpendicular to the line.
Why is that point closest? Move the candidate point along the line. The perpendicular gap stays the same, but an extra sideways gap appears. Those two gaps form a right triangle. Its squared distance is the perpendicular gap squared plus the extra gap squared. Adding a nonzero extra gap can only increase the distance.
This gives us the nearest point on the infinite line through A and B. It may lie before A or beyond B. A segment needs one more restriction.
Use a fraction of the segment
Chapter 05 placed a point with a + t * (b - a). We can express our projection using that same dimensionless parameter:
float t = along / length(ab); // 1.2 / 1.5 = 0.8
vec2 closest = a + t * ab;
Substitute u = ab / length(ab) into the expression for along. We divide by the segment length twice, so the denominator becomes its squared length:
float t = dot(p - a, ab) / dot(ab, ab);
// dot(p - a, ab) = 1.8
// dot(ab, ab) = 2.25
// t = 1.8 / 2.25 = 0.8
Both dot products have squared coordinate units. Their ratio has no units, exactly what we need for a fraction. This form does not need a square root or a normalized vector. The denominator must still be nonzero.
Dividing by length(ab) here would be a mistake. The numerator uses the raw ab, not its unit direction. With our values, that mistake gives 1.2 instead of 0.8 and places the candidate beyond B.
Stop at the endpoints
For a segment, clamp the fraction before constructing the closest point:
float usedT = clamp(t, 0.0, 1.0);
vec2 closest = a + usedT * ab;
float distanceToSegment = length(p - closest);
If the projection lies between the endpoints, nothing changes. If t < 0, the nearest allowed point is A. If t > 1, it is B.
| Probe | Raw t | Closest on line | Closest on segment |
|---|---|---|---|
| (-0.66, -0.82) | -0.2 | (-0.84, -0.58) | A |
| (0.66, 0.92) | 1.2 | (0.84, 0.68) | B |
Both probes are 0.3 units from the infinite line. Their distances to the segment are larger: about 0.42426. After clamping to an endpoint, the residual is generally not perpendicular to the line. The closest point is constrained by the segment’s ends.
Handle a collapsed segment
When A and B coincide, ab and its squared length are zero. The segment is now one point. Use A as the closest point and skip the division:
vec2 ab = b - a;
float squaredLength = dot(ab, ab);
vec2 closest = a;
if (squaredLength > 0.00000001) {
float t = dot(p - a, ab) / squaredLength;
closest = a + clamp(t, 0.0, 1.0) * ab;
}
float distanceToSegment = length(p - closest);
The threshold is in squared coordinate units. In this small scene, we also treat segments of length about 0.0001 or less as a point to avoid dividing by a tiny number. That is an explicit approximation, not a new projection formula. Float rounding can affect the exact threshold comparison.
Coincident points do not define an infinite line. Our Line mode uses the same point fallback and labels it clearly. It does not invent a direction.