# 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. # # Interpolation objects are constructed with a Hash object, wherein each key # is a real number value and each value can respond to +interpolate+ to # determine the resulting value based on its neighbor value and the balance # ratio between the two points. # # For objects which can't respond to +interpolate+ (or to override the default # behaviour), a block can be passed to +new+ which will be called whenever two # values need to be interpolated. # # 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] # # # ==License # # Licensed under the MIT license. # class Interpolation VERSION = '0.2.3' # creates an Interpolation object with Hash object that specifies # each point location (Numeric) and value (up to you) # # the optional+block+ can be used to interpolate objects that can't # respond to +interpolate+ on their own # # +block+ will receive the following arguments: "left" (lower) side # value, "right" (higher) side value, and the balance ratio from 0.0 # to 1.0 def initialize(points = {}, &block) @points = {} @block = block merge!(points) end # creates an Interpolation object from the receiver object, # merged with the interpolated points you specify def merge(points = {}) Interpolation.new(points.merge(@points)) end # merges the interpolation points with the receiver object def merge!(points = {}) @points.merge!(points) normalize_data end # returns the interpolated value of the receiver object at the point specified 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) # given block should be called return @block.call(left_value, right_value, balance) if @block # otherwise, we need to interpolate return left_value.interpolate(right_value, balance) if left_value.respond_to?(:interpolate) raise ArgumentError, "no block given and interpolation point doesn't respond to :interpolate" 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 # :nodoc: @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 end end end