=begin rdoc Library for generic interpolation objects. Useful for such things as generating linear motion between points (or arrays of points), multi-channel color gradients, piecewise functions, or even just placing values within intervals. The only requirement is that each interpolation point value must be able to figure out how to interpolate itself to its neighbor value(s). Numeric objects and uniformly sized arrays are automatically endowed with this ability by this gem, but other classes will require an implementation of #interpolate. See the second example below for a brief demonstration using Color objects. Interpolation objects are constructed with a Hash object, wherein each key is a real number value and each value is can respond to #interpolate and determine the resulting value based on its neighbor value and the balance ratio between the two points. At or below the lower bounds of the interpolation, the result will be equal to the value of the lower bounds interpolation point. At or above the upper bounds of the graient, the result will be equal to the value of the upper bounds interpolation point. ==Author {Adam Collins}[mailto:adam.w.collins@gmail.com] ==General Usage Specify the interpolation as a Hash, where keys represent numeric points along the gradient and values represent the known values along that gradient. Here's an example for determining which of 7 zones a set of values fall into: require 'rubygems' require 'interpolate' points = { 0.000 => 0, 0.427 => 1, 1.200 => 2, 3.420 => 3, 27.50 => 4, 45.20 => 5, 124.4 => 6, } zones = Interpolation.new(points) values = [ -20.2, 0.234, 65.24, 9.234, 398.4, 4000 ] values.each do |value| zone = zones.at(value).floor puts "A value of #{value} falls into zone #{zone}" end ==Non-Numeric Gradients For non-Numeric gradient value objects, you'll need to implement :interpolate for the class in question. Here's an example using an RGB color gradient with the help of the 'color' gem: require 'rubygems' require 'interpolate' require 'color' # we need to implement :interpolate for Color::RGB # in order for Interpolation to work class Color::RGB def interpolate(other, balance) mix_with(other, balance * 100.0) end end # a nice weathermap-style color gradient points = { 0 => Color::RGB::White, 1 => Color::RGB::Lime, # 2 => ? (something between Lime and Yellow) 3 => Color::RGB::Yellow, 4 => Color::RGB::Orange, 5 => Color::RGB::Red, 6 => Color::RGB::Magenta, 7 => Color::RGB::DarkGray } gradient = Interpolation.new(points) # what are the colors of the gradient from 0 to 7 # in increments of 0.2? (0).step(7, 0.2) do |value| color = gradient.at(value) puts "A value of #{value} means #{color.html}" end ==Array-based Interpolations Aside from single value gradient points, you can interpolate over uniformly sized arrays. Between two interpolation points, let's say +a+ and +b+, the final result will be +c+ where +c[0]+ is the interpolation of +a[0]+ and +b[0]+ and +c[1]+ is interpolated between +a[1]+ and +b[1]+ and so on up to +c[n]+. Here is an example: require 'rubygems' require 'interpolate' require 'pp' # a non-linear set of multi-dimensional points; # perhaps the location of some actor in relation to time time_frames = { 0 => [0, 0, 0], 1 => [1, 0, 0], 2 => [0, 1, 0], 3 => [0, 0, 2], 4 => [3, 0, 1], 5 => [1, 2, 3], 6 => [0, 0, 0] } path = Interpolation.new(time_frames) # play the actors positions in time increments of 0.25 (0).step(6, 0.25) do |time| position = path.at(time) puts ">> At #{time}s, actor is at:" p position end ==Nested Array Interpolations As long as each top level array is uniformly sized in the first dimension and each nested array is uniformly sized in the second dimension (and so on...), multidimensional interpolation point values will just work. Here's an example of a set of 2D points being morphed: require 'rubygems' require 'interpolate' require 'pp' # a non-linear set of 2D vertexes; # the shape changes at each frame time_frames = { 0 => [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]], # a horizontal line 1 => [[0, 0], [1, 0], [3, 0], [0, 4], [0, 0]], # a triangle 2 => [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], # a square 3 => [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]], # a horizontal line, again 4 => [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]] # a vertical line } paths = Interpolation.new(time_frames) # show the vertex positions in time increments of 0.25 (0).step(4, 0.25) do |time| points = paths.at(time) puts ">> At #{time}s, points are:" p points end ==License Licensed under the MIT license. =end # all numeric objects should be supported out of the box class Numeric def interpolate(other, balance) left = self.to_f right = other.to_f delta = (right - left).to_f return left + (delta * balance) end end # a little more complicated, but there's no reason why we can't # interpolate between two equal length arrays as long as each element # responds to :interpolate class Array def interpolate(other, balance) if (self.length < 1) then raise ArgumentError, "cannot interpolate array with no values" end if (self.length != other.length) then raise ArgumentError, "cannot interpolate between arrays of different length" end final = Array.new self.each_with_index do |left, index| unless (left.respond_to? :interpolate) then raise "array element does not respond to :interpolate" end right = other[index] final[index] = left.interpolate(right, balance) end return final end end class Interpolation VERSION = '0.2.0' def initialize(points = {}) @points = {} add!(points) end def add(points = {}) Interpolation.new(points.merge(@points)) end def add!(points = {}) @points.merge!(points) normalize_data end def at(point) # deal with the two out-of-bounds cases first if (point <= @min_point) return @data.first.last elsif (point >= @max_point) return @data.last.last end # go through the interpolation intervals, in order, to determine # into which this point falls 1.upto(@data.length - 1) do |zone| left = @data.at(zone - 1) right = @data.at(zone) zone_range = left.first..right.first if (zone_range.include?(point)) # what are the points in question? left_point = left.first.to_f right_point = right.first.to_f # what are the values in question? left_value = left.last right_value = right.last # span: difference between the left point and right point # balance: ratio of right point to left point span = right_point - left_point balance = (point.to_f - left_point) / span # catch the cases where the point in quesion is # on one of the zone's endpoints return left_value if (balance == 0.0) return right_value if (balance == 1.0) # otherwise, we need to interpolate return left_value.interpolate(right_value, balance) end end # we shouldn't get to this point raise "couldn't come up with a value for some reason!" end private def normalize_data @data = @points.sort @min_point = @data.first.first @max_point = @data.last.first # make sure that all values respond_to? :interpolate @data.each do |point| value = point.last unless value.respond_to?(:interpolate) raise ArgumentError, "found an interpolation point that doesn't respond to :interpolate" end end end end