In which, we try a smaller object on for size. Doesn’t seem to fit.
In the previous article, we looked at two methods of L_Bezier, and observed that they could be improved by limiting each one to manipulating just one of the two tables in L_Bezier.
What if we pushed that idea even further? What if we had an object that only dealt with finding interpolation index and fraction, and another separate object, or several objects, that used that result and did actual interpolation of positions or, well, anything?
I think that’s an interesting notion. Let’s find out.
Let’s start with the one that has the table of lengths and that uses them to produce an index into the table and a fraction. This object doesn’t need to know that it is dealing with lengths. If could process anything that follows the rules.
I’m not sure what to call it. Let’s call it, oh, Interpolator, and let’s begin with a test. This is too much but not too much too much:
function Tests:test_interpolator()
local values = {0, 10, 20, 50, 70}
local int = Interpolator(values)
local index, fraction
index, fraction = int:at(5)
self:assert_equals(index, 2)
self:assert_equals(fraction, 0.5)
end
Why too much? Well, I’m not even sure yet just what form the values should take. As shown, they’re increasing. But my experience with L_Bezier makes me think I want them to be the individual lengths of the segments between. Let’s try it that way.
local values = {0, 10, 10, 30, 20}
OK, and let’s have this much code:
Interpolator = class()
function Interpolator:init(values)
self._values = values
end
Test will fail looking for at
. It does. Code it:
function Interpolator:at(desired)
local remaining_value = desired
for i, value in self._values do
if remaining_value < value then
return i, remaining_value/value
end
remaining_value -= value
end
return nil
end
Obviously, I cribbed this from L_Bezier and massaged the names a bit.
Let’s add a test or check or two but I think we’re really done here.
function Tests:test_interpolator_more()
local values = {0, 10, 10, 30, 20}
local int = Interpolator(values)
local index, fraction
index, fraction = int:at(3)
self:assert_equals(index, 2)
self:assert_equals(fraction, 0.3)
index, fraction = int:at(60)
self:assert_equals(index, 5)
self:assert_equals(fraction, 0.5)
end
These pass. Ship it. We have a nice new tiny object, an interpolator.
For our next trick, we’ll try installing Interpolator into L_Bezier and see how it feels.
I found it desirable to provide an add
method to Interpolate, so that I could easily plug it into L_Bezier:
Interpolator = class()
function Interpolator:init(values)
self._values = values or {}
end
function Interpolator:add(value)
table.insert(self._values, value)
end
function Interpolator:at(desired)
local remaining_value = desired
for i, value in self._values do
if remaining_value < value then
return i, remaining_value/value
end
remaining_value -= value
end
return nil
end
And installed in L_Bezier:
L_Bezier = class()
function L_Bezier:init(beziers)
self.length = 0
self._interp = Interpolator() -- <---
self._points = {}
self.ready = false
for _, b in beziers do
self:_add(b.p0)
self:_add(b.p1)
self:_add(b.p2)
-- self:_add(b.p3)
end
self:_add(beziers[#beziers].p3)
end
function L_Bezier:_add(point)
local len
local n = #self._points
if n == 0 then
len = 0
else
len = point:dist(self._points[n])
end
self.length += len
self._interp:add(len) -- <---
table.insert(self._points, point)
end
-- just here supporting some tests.
function L_Bezier:find_distance(d)
return self._interp:at(d)
end
-- to be removed. covered by Interpolator tests.
function L_Bezier:point_at_distance(d, debug)
local idx, frac = self._interp:at(d) -- <---
local p0 = self._points[idx-1]
local p1 = self._points[idx]
return p0*(1 - frac) + p1*frac
end
What I like about this is that we could use the interpolator in lots of places, if we had use for it, and it would be useful in all those places: we wouldn’t have to duplicate the find code.
What I do not like, as things stand, is that I needed to create and use the incremental add
method, instead of just creating the interpreter once and for all, giving it all the values at once.
So my current, not at all final answer, is that I like the Interpolator, but it isn’t quite as useful, yet, as I had hoped. So we’ll keep on trying.
Safe paths!