shawn42/gamebox

View on GitHub
lib/gamebox/lib/vector2.rb

Summary

Maintainability
C
1 day
Test Coverage
#++
# Copyright (C) 2008-2010  John Croisant
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions: # 
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.  # 
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# The Vector2 class implements two-dimensional vectors.
# It is used to represent positions, movements, and velocities
# in 2D space.
# 
class Vector2
  include Enumerable
  include MinMaxHelpers

  RAD_TO_DEG = 180.0 / Math::PI
  DEG_TO_RAD = Math::PI / 180.0
  MAX_UDOT_PRODUCT = 1.0
  MIN_UDOT_PRODUCT = -1.0

  class << self

    alias :[] :new


    # Creates a new Vector2 from an angle in radians and a
    # magnitude. Use #new_polar_deg for degrees.
    # 
    def new_polar( angle_rad, magnitude )
      self.new( Math::cos(angle_rad)*magnitude,
                Math::sin(angle_rad)*magnitude )
    end


    # Creates a new Vector2 from an angle in degrees and a
    # magnitude. Use #new_polar for radians.
    # 
    def new_polar_deg( angle_deg, magnitude )
      self.new_polar( angle_deg * DEG_TO_RAD, magnitude )
    end


    # call-seq:
    #   Vector2.many( [x1,y1], [x2,y2], ... )
    # 
    # Converts multiple [x,y] Arrays to Vector2s.
    # Returns the resulting vectors in an Array.
    # 
    def many( *pairs )
      pairs.collect { |pair| self.new(*pair) }
    end

  end


  # Creates a new Vector2 with the given x and y values.
  def initialize( x, y )
    @x, @y = x.to_f, y.to_f
  end

  attr_reader :x, :y

  def x=( value )
    raise "can't modify frozen object" if frozen?
    @x = value.to_f
    @hash = nil
    self
  end

  def y=( value )
    raise "can't modify frozen object" if frozen?
    @y = value.to_f
    @hash = nil
    self
  end


  # Sets this vector's x and y components.
  def set!( x, y )
    raise "can't modify frozen object" if frozen?
    @x = x.to_f
    @y = y.to_f
    @hash = nil
    self
  end


  # Sets this vector's angle (in radians) and magnitude.
  # Use #set_polar_deg! for degrees.
  # 
  def set_polar!( angle_rad, mag )
    raise "can't modify frozen object" if frozen?
    @x = Math::cos(angle_rad) * mag
    @y = Math::sin(angle_rad) * mag
    @hash = nil
    self
  end


  # Sets this vector's angle (in degrees) and magnitude.
  # Use #set_polar! for radians.
  # 
  def set_polar_deg!( angle_deg, mag )
    set_polar!( angle_deg * DEG_TO_RAD, mag )
    self
  end


  # Adds the given vector to this one and return the
  # resulting vector.
  # 
  def +( vector )
    self.class.new( @x + vector.at(0), @y + vector.at(1) )
  end

  # call-seq:
  #   move!( [x,y] )
  #   move!( x,y )
  # 
  # Moves the vector by the given x and y amounts.
  def move!( x, y=nil )
    raise "can't modify frozen object" if frozen?
    if y.nil?
      a = x.to_ary
      @x += a[0]
      @y += a[1]
    else
      @x += x
      @y += y
    end
    @hash = nil
    self
  end

  # call-seq:
  #   move( [x,y] )
  #   move( x,y )
  # 
  # Like #move!, but returns a new vector.
  def move( x, y=nil )
    self.dup.move!(x, y)
  end


  # Subtracts the given vector from this one and return
  # the resulting vector.
  # 
  def -( vector )
    self.class.new( @x - vector.at(0), @y - vector.at(1) )
  end


  # Returns the opposite of this vector, i.e. Vector2[-x, -y].
  def -@
    self.class.new( -@x, -@y )
  end

  # Reverses the vector's direction, i.e. Vector2[-x, -y].
  def reverse!
    raise "can't modify frozen object" if frozen?
    @x, @y = -@x, -@y
    @hash = nil
    self
  end

  # Like #reverse!, but returns a new vector.
  def reverse
    self.dup.reverse!
  end


  # Multiplies this vector by the given scalar (Numeric),
  # and return the resulting vector.
  # 
  def *( scalar )
    self.class.new( @x * scalar, @y * scalar )
  end


  # True if the given vector's x and y components are
  # equal to this vector's components (within a small margin
  # of error to compensate for floating point imprecision).
  # 
  def ==( vector )
    return false if vector.nil?
    Vector2.vector_nearly_equal?(self, vector)
  end


  # Returns a component of this vector as if it were an
  # [x,y] Array.
  # 
  def []( index )
    case index
    when 0
      @x
    when 1
      @y
    else
      nil
    end
  end

  alias :at :[]


  def hash # :nodoc:
    @hash ||= [@x, @y, self.class].hash
  end


  # Iterates over this vector as if it were an [x,y] Array.
  # 
  def each
    yield @x
    yield @y
  end


  # Returns the angle of this vector, relative to the positive
  # X axis, in radians. Use #angle_deg for degrees.
  # 
  def angle
    Math.atan2( @y, @x )
  end


  # Sets the vector's angle in radians. The vector keeps the same
  # magnitude as before.
  # 
  def angle=( angle_rad )
    raise "can't modify frozen object" if frozen?
    m = magnitude
    @x = Math::cos(angle_rad) * m
    @y = Math::sin(angle_rad) * m
    @hash = nil
    self
  end


  # Returns the angle of this vector relative to the other vector,
  # in radians. Use #angle_deg_with for degrees.
  # 
  def angle_with( vector )
    Math.acos( max(min(udot(vector),MAX_UDOT_PRODUCT), MIN_UDOT_PRODUCT) )
  end


  # Returns the angle of this vector, relative to the positive
  # X axis, in degrees. Use #angle for radians.
  # 
  def angle_deg
    angle * RAD_TO_DEG
  end


  # Sets the vector's angle in degrees. The vector keeps the same
  # magnitude as before.
  # 
  def angle_deg=( angle_deg )
    self.angle = angle_deg * DEG_TO_RAD
    self
  end


  # Returns the angle of this vector relative to the other vector,
  # in degrees. Use #angle_with for radians.
  # 
  def angle_deg_with( vector )
    angle_with(vector) * RAD_TO_DEG
  end


  # Returns the dot product between this vector and the other vector.
  def dot( vector )
    (@x * vector.at(0)) + (@y * vector.at(1))
  end


  # Returns the cross product between this vector and the other vector.
  def cross( vector )
    (@x * vector.y) - (@y * vector.x)
  end


  # Returns the magnitude (distance) of this vector.
  def magnitude
    Math.hypot( @x, @y )
  end


  # Sets the vector's magnitude (distance). The vector keeps the
  # same angle as before.
  # 
  def magnitude=( mag )
    raise "can't modify frozen object" if frozen?
    angle_rad = angle
    @x = Math::cos(angle_rad) * mag
    @y = Math::sin(angle_rad) * mag
    @hash = nil
    self
  end


  # Returns a copy of this vector, but rotated 90 degrees
  # counter-clockwise.
  # 
  def perp
    self.class.new( -@y, @x )
  end

  # Sets this vector to the vector projection (aka vector resolute)
  # of this vector onto the other vector. See also #projected_onto.
  # 
  def project_onto!( vector )
    raise "can't modify frozen object" if frozen?
    b = vector.unit
    @x, @y = *(b.scale(self.dot(b)))
    @hash = nil
    self
  end


  # Like #project_onto!, but returns a new vector.
  def projected_onto( vector )
    dup.project_onto!( vector )
  end


  # Rotates the vector the given number of radians.
  # Use #rotate_deg! for degrees.
  # 
  def rotate!( angle_rad )
    self.angle += angle_rad
    self
  end

  # Like #rotate!, but returns a new vector.
  def rotate( angle_rad )
    dup.rotate!( angle_rad )
  end


  # Rotates the vector the given number of degrees.
  # Use #rotate for radians.
  # 
  def rotate_deg!( angle_deg )
    self.angle_deg += angle_deg
    self
  end

  # Like #rotate_deg!, but returns a new vector.
  def rotate_deg( angle_deg )
    dup.rotate_deg!( angle_deg )
  end


  # call-seq:
  #   scale!( scale )
  #   scale!( scale_x, scale_y )
  # 
  # Multiplies this vector's x and y values.
  #
  # If one number is given, the vector will be equal to
  # Vector2[x*scale, y*scale]. If two numbers are given, it will be
  # equal to Vector2[x*scale_x, y*scale_y].
  # 
  # Example:
  # 
  #   v = Vector2[1.5,2.5]
  #   v.scale!( 2 )           # => Vector2[3,5]
  #   v.scale!( 3, 4 )        # => Vector2[9,20]
  # 
  def scale!( scale_x, scale_y = scale_x )
    raise "can't modify frozen object" if frozen?
    @x, @y = @x * scale_x, @y * scale_y
    @hash = nil
    self
  end

  # Like #scale!, but returns a new vector.
  def scale( scale_x, scale_y = scale_x )
    dup.scale!(scale_x, scale_y)
  end


  # Returns this vector as an [x,y] Array.
  def to_ary
    [@x, @y]
  end

  alias :to_a :to_ary


  def to_s
    "Vector2[#{@x}, #{@y}]"
  end

  alias :inspect :to_s


  # Returns the dot product of this vector's #unit and the other
  # vector's #unit.
  # 
  def udot( vector )
    unit.dot( vector.unit )
  end


  # Sets this vector's magnitude to 1.
  def unit!
    raise "can't modify frozen object" if frozen?
    scale = 1/magnitude
    @x, @y = @x * scale, @y * scale
    @hash = nil
    self
  end

  alias :normalize! :unit!

  # Like #unit!, but returns a new vector.
  def unit
    self.dup.unit!
  end

  alias :normalized :unit

  # Checks whether it is a vector or not.Useful for debugging
  def self.expect(vector)
    raise "expected type of Vector2, got #{vector.inspect}" unless vector.is_a?(Vector2)
    vector
  end

  # Check whether vectors are same according to toleranceRate.By default tolerances are set to 10 ** -10 = 0.0000000001
  def self.vector_nearly_equal?(first_vector,second_vector,toleranceRateForMagnitude = 1E-10,toleranceRateForAngle = 1E-10)
    return (magnitude_nearly_equal?(first_vector,second_vector,toleranceRateForMagnitude) and
            angle_nearly_equal?(first_vector,second_vector,toleranceRateForAngle))
  end

  # Check whether vectors' magnitude are same according to toleranceRate
  def self.magnitude_nearly_equal?(first_vector,second_vector,toleranceRate)
    return (first_vector-second_vector).magnitude <= toleranceRate
  end

  # Check whether vectors' angle are same according to toleranceRate
  def self.angle_nearly_equal?(first_vector,second_vector,toleranceRate)
    return (first_vector-second_vector).angle.abs <= toleranceRate
  end

  # Returns distance between 2 vectors
  def distance(vector)
    Math.sqrt((@x - vector.x)**2 + (@y - vector.y)**2)
  end

  # Returns  copy of this vector
  def copy
    self.class.new(@x,@y)
  end

  # Limits vector's magnitude by a maximum value
  def limit(maximum)
    mag_squared = magnitude ** 2
    return copy if mag_squared <= maximum**2
    return (unit! * maximum)
  end

  #Linearly interpolates a value between a and b with respect to T
  def self.lerp(a, b, t)
    a + (b- a) * t
  end

  #Linearly interpolates a value between old vector values and selected vector values with respect to T and returns new vector
  def lerp_vector(vector, t)
    self.class.new(Vector2.lerp(@x, vector.x, t),
                   Vector2.lerp(@y, vector.y, t))
  end

  #Clamps a value between min and max
  def self.clamp(value, min, max)
    return min if value <= min
    return max if value >= max
    return input
  end

  #Clamps vectors values with respect to minimum vector and maximum vector and returns a new vector
  def clamp_vector(min_vector, max_vector)
    self.class.new(Vector2.clamp(@x, min_vector.x, max_vector.x),
                   Vector2.clamp(@y, min_vector.y, max_vector.y))
  end

end