lib/interpolate.rb in interpolate-0.2.0 vs lib/interpolate.rb in interpolate-0.2.1

- old
+ new

@@ -1,247 +1,60 @@ -=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 example color.rb in the examples directory for +# a brief demonstration using Color objects provided by the 'color' gem. +# +# 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] +# +# +# ==License +# +# Licensed under the MIT license. +# -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' + VERSION = '0.2.1' # :nodoc: + # creates an Interpolation object with Hash object that specifies + # each point location (Numeric) and value (up to you) def initialize(points = {}) @points = {} - add!(points) + merge!(points) end - def add(points = {}) + # creates an Interpolation object from the receiver object, + # merged with the interpolated points you specify + def merge(points = {}) Interpolation.new(points.merge(@points)) end - def add!(points = {}) + # 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) @@ -283,11 +96,11 @@ raise "couldn't come up with a value for some reason!" end private - def normalize_data + 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 @@ -300,5 +113,44 @@ end end +# all numeric objects should be supported +class Numeric # :nodoc: + 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 # :nodoc: + 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