Summary
My simple function performing a binary search for a local minimum often returns spurious values corresponding to exactly ¼, ½, and ¾ of the search. Why is this happening?
/**
* minX : the smallest input value
* maxX : the largest input value
* ƒ : a function that returns a value `y` given an `x`
* ε : how close in `x` the bounds must be before returning
* return: the `x` value that produces the smallest `y`
*/
function localMinimum(minX, maxX, ƒ, ε) {
if (ε===undefined) ε=1e-15;
let m=minX, n=maxX, k;
while ((n-m)>ε) {
k = (n+m)/2;
if (ƒ(m)<ƒ(n)) n=k;
else m=k;
}
return k;
}
Live Demo: http://phrogz.net/svg/closest-point-on-bezier_broken.html
The demo lets you adjust a cubic Bézier curve, move the mouse around, and attempts to find a point on the curve closest to the mouse location. In the screenshots below, you can see the curve (black), the location of the mouse (centered in the gray circle), the location found on the curve (red dot), a brute-force graph of the distance from the cursor to each point on the curve (lower right corner), and the t parameter that was calculated as the local minimum (red text).
These screenshots represent the worst case. In them the mouse location is very close to the line, and the "local minimum" function is returning a point that is NOT the local minimum. If you experiment with the demo, you will see that in many cases the local minimum function is working as intended. Just…not near some of the early binary search bounds.
Guide to the Code
The localMinimum()
function (line 225) is called by the closestPoint()
function (line 194):
/**
* out : A vmath vector to modify with the point value
* curve : Array of vectors as control points for a Bézier curve (any degree)
* pt : Object with .x/.y to find the point closest to
* return: A parameter t representing the location of `out`
*/
function closestPoint(out, curve, pt, tmps) {
let vec=vmath[ 'w' in curve[0] ? 'vec4' : 'z' in curve[0] ? 'vec3' : 'vec2' ];
return localMinimum(0, 1,
t => vec.squaredDistance(pt, bézierPoint(out, curve, t)));
}
The bézierPoint()
function (line 207) uses De Casteljau's algorithm to calculate a point along the curve parameterized by t. This function works as expected, tested independently of the rest of the problem.
The squaredDistance()
function is part of the vmath library, and is as simple as you would imagine.
What am I missing? How is my local minima function failing?