gosling 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,31 @@
1
1
  require 'singleton'
2
2
 
3
+ require_relative 'utils.rb'
4
+
3
5
  module Gosling
6
+ ##
7
+ # Very basic 2D collision detection. It is naive to where actors were during the last physics step or how fast they are
8
+ # moving. But it does a fine job of detecting collisions between actors in their present state.
9
+ #
10
+ # Keep in mind that Actors and their subclasses each have their own unique shapes. Actors, by themselves, have no
11
+ # shape and will never collide with anything. To see collisions in action, you'll need to use Circle, Polygon, or
12
+ # something else that has an actual shape.
13
+ #
14
+
4
15
  class Collision
5
- # 11.4 - 6.7 (get_separation_axes) - 4.7 (project_onto_axis)
16
+ include Singleton
17
+
18
+ ##
19
+ # Tests two Actors or child classes to see whether they overlap. Actors, having no shape, never overlap. Child
20
+ # classes use appropriate algorithms based on their shape.
21
+ #
22
+ # Arguments:
23
+ # - shapeA: an Actor
24
+ # - shapeB: another Actor
25
+ #
26
+ # Returns:
27
+ # - true if the actors' shapes overlap, false otherwise
28
+ #
6
29
  def self.test(shapeA, shapeB)
7
30
  return false if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
8
31
 
@@ -19,8 +42,19 @@ module Gosling
19
42
  return true
20
43
  end
21
44
 
45
+ ##
46
+ # Tests a point in space to see whether it is inside the actor's shape or not.
47
+ #
48
+ # Arguments:
49
+ # - point: a Snow::Vec3
50
+ # - shape: an Actor
51
+ #
52
+ # Returns:
53
+ # - true if the point is inside of the actor's shape, false otherwise
54
+ #
22
55
  def self.is_point_in_shape?(point, shape)
23
- raise ArgumentError.new("Collision.get_normal() requires a point and an actor") unless point.is_a?(Vector) && point.size == 3 && shape.is_a?(Actor)
56
+ type_check(point, Snow::Vec3)
57
+ type_check(shape, Actor)
24
58
 
25
59
  return false if shape.instance_of?(Actor)
26
60
 
@@ -34,24 +68,24 @@ module Gosling
34
68
 
35
69
  separation_axes.each do |axis|
36
70
  shape_projection = project_onto_axis(shape, axis)
37
- point_projection = point.inner_product(axis)
71
+ point_projection = point.dot_product(axis)
38
72
  return false unless shape_projection.min <= point_projection && point_projection <= shape_projection.max
39
73
  end
40
74
 
41
75
  return true
42
76
  end
43
77
 
44
- def self.get_normal(vector)
45
- raise ArgumentError.new("Collision.get_normal() requires a length 3 vector") unless vector.is_a?(Vector) && vector.size == 3
46
- raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector.magnitude == 0
78
+ private
47
79
 
48
- Vector[-vector[1], vector[0], 0]
80
+ def self.get_normal(vector)
81
+ type_check(vector, Snow::Vec3)
82
+ raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector.magnitude_squared == 0
83
+ Snow::Vec3[-vector[1], vector[0], 0]
49
84
  end
50
85
 
51
86
  def self.get_polygon_separation_axes(vertices)
52
- unless vertices.is_a?(Array) && vertices.reject { |v| v.is_a?(Vector) && v.size == 3 }.empty?
53
- raise ArgumentError.new("Collission.get_polygon_separation_axes() expects an array of vectors similar to that produced by Polygon.get_vertices")
54
- end
87
+ type_check(vertices, Array)
88
+ vertices.each { |v| type_check(v, Snow::Vec3) }
55
89
 
56
90
  axes = (0...vertices.length).map do |i|
57
91
  axis = vertices[(i + 1) % vertices.length] - vertices[i]
@@ -61,14 +95,12 @@ module Gosling
61
95
  end
62
96
 
63
97
  def self.get_circle_separation_axis(circleA, circleB)
64
- unless circleA.is_a?(Actor) && circleB.is_a?(Actor)
65
- raise ArgumentError.new("Collision.get_circle_separation_axis() expects two circles")
66
- end
98
+ type_check(circleA, Actor)
99
+ type_check(circleB, Actor)
67
100
  axis = circleB.get_global_position - circleA.get_global_position
68
101
  (axis.magnitude > 0) ? axis.normalize : nil
69
102
  end
70
103
 
71
- # 4.8 - 2.6 - 1.6 - .4
72
104
  def self.get_separation_axes(shapeA, shapeB)
73
105
  unless shapeA.is_a?(Actor) && !shapeA.instance_of?(Actor)
74
106
  raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeA.inspect}!")
@@ -98,25 +130,28 @@ module Gosling
98
130
  end
99
131
 
100
132
  def self.project_onto_axis(shape, axis)
101
- raise ArgumentError.new("Expected Actor, but received #{shape.inspect}!") unless shape.is_a?(Actor)
102
- raise ArgumentError.new("Expected Vector, but received #{shape.inspect}!") unless axis.is_a?(Vector)
133
+ type_check(shape, Actor)
134
+ type_check(axis, Snow::Vec3)
103
135
 
104
136
  global_vertices = if shape.instance_of?(Circle)
105
137
  global_tf = shape.get_global_transform
106
- local_axis = global_tf.inverse * Vector[axis[0], axis[1], 0]
138
+ local_axis = global_tf.inverse * Snow::Vec3[axis[0], axis[1], 0]
107
139
  v = shape.get_point_at_angle(Math.atan2(local_axis[1], local_axis[0]))
108
140
  [v, v * -1].map { |vertex| Transform.transform_point(global_tf, vertex) }
109
141
  else
110
142
  shape.get_global_vertices
111
143
  end
112
144
 
113
- projections = global_vertices.map { |vertex| vertex.inner_product(axis) }.sort
145
+ projections = global_vertices.map { |vertex| vertex.dot_product(axis) }.sort
114
146
  [projections.first, projections.last]
115
147
  end
116
148
 
117
149
  def self.projections_overlap?(a, b)
118
- raise ArgumentError.new("Collision.projections_overlap?() expects arrays") unless a.is_a?(Array) && b.is_a?(Array)
119
- raise ArgumentError.new("Collision.projections_overlap?() projection arrays must be length 2") unless a.length == 2 && b.length == 2
150
+ type_check(a, Array)
151
+ type_check(b, Array)
152
+ a.each { |x| type_check(x, Numeric) }
153
+ b.each { |x| type_check(x, Numeric) }
154
+ raise ArgumentError.new("Expected two arrays of length 2, but received #{a.inspect} and #{b.inspect}!") unless a.length == 2 && b.length == 2
120
155
 
121
156
  a.sort! if a[0] > a[1]
122
157
  b.sort! if b[0] > b[1]
@@ -1,11 +1,18 @@
1
1
  require 'singleton'
2
2
 
3
3
  module Gosling
4
+ ##
5
+ # A cached Gosu::Image repository.
6
+ #
4
7
  class ImageLibrary
5
8
  @@cache = {}
6
9
 
7
10
  include Singleton
8
11
 
12
+ ##
13
+ # When passed the path to an image, it first checks to see if that image is in the cache. If so, it returns the cached
14
+ # Gosu::Image. Otherwise, it loads the image, stores it in the cache, and returns it.
15
+ #
9
16
  def self.get(filename)
10
17
  raise ArgumentError.new("File not found: '#{filename}' in '#{Dir.pwd}'") unless File.exists?(filename)
11
18
  unless @@cache.has_key?(filename)
@@ -0,0 +1,42 @@
1
+ require 'snow-math'
2
+
3
+ module Snow
4
+ class Mat3
5
+ ##
6
+ # Monkey-patch fix for Mat3 * Vec3
7
+ #
8
+ def multiply(rhs, out = nil)
9
+ case rhs
10
+ when ::Snow::Mat3
11
+ multiply_mat3(rhs, out)
12
+ when ::Snow::Vec3
13
+ values = (0..2).map { |i| get_row3(i) ** rhs }
14
+ out ||= Snow::Vec3.new
15
+ out.set(values)
16
+ when Numeric
17
+ scale(rhs, rhs, rhs, out)
18
+ else
19
+ raise TypeError, "Invalid type for RHS"
20
+ end
21
+ end
22
+ alias_method :*, :multiply
23
+
24
+ ##
25
+ # Monkey-patch fix for #multiply! type-switching
26
+ #
27
+ def multiply!(rhs)
28
+ multiply rhs, case rhs
29
+ when ::Snow::Mat3, Numeric then self
30
+ when ::Snow::Vec3 then rhs
31
+ else raise TypeError, "Invalid type for RHS"
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Returns true if this Mat3 is an identity matrix.
37
+ #
38
+ def identity?
39
+ [1, 2, 3, 5, 6, 7].all? { |i| self[i] == 0 } && [0, 4, 8].all? { |i| self[i] == 1 }
40
+ end
41
+ end
42
+ end
@@ -1,35 +1,67 @@
1
1
  require_relative 'actor.rb'
2
2
  require_relative 'collision.rb'
3
+ require_relative 'utils.rb'
3
4
 
4
5
  module Gosling
6
+ ##
7
+ # A Polygon is an Actor with a shape defined by three or more vertices. Can be used to make triangles, hexagons, or
8
+ # any other unusual geometry not covered by the other Actors. For circles, you should use Circle. For squares or
9
+ # rectangles, see Rect.
10
+ #
5
11
  class Polygon < Actor
12
+ ##
13
+ # Creates a new, square Polygon with a width and height of 1.
14
+ #
6
15
  def initialize(window)
16
+ type_check(window, Gosu::Window)
7
17
  super(window)
8
18
  @vertices = [
9
- Vector[0, 0, 0],
10
- Vector[1, 0, 0],
11
- Vector[1, 1, 0],
12
- Vector[0, 1, 0]
19
+ Snow::Vec3[0, 0, 0],
20
+ Snow::Vec3[1, 0, 0],
21
+ Snow::Vec3[1, 1, 0],
22
+ Snow::Vec3[0, 1, 0]
13
23
  ]
14
24
  end
15
25
 
26
+ ##
27
+ # Returns a copy of this Polygon's vertices (@vertices is read-only).
28
+ #
16
29
  def get_vertices
17
30
  @vertices.dup
18
31
  end
19
32
 
33
+ ##
34
+ # Sets this polygon's vertices. Requires three or more Snow::Vec3.
35
+ #
36
+ # Usage:
37
+ # - polygon.set_vertices([Snow::Vec3[-1, 0, 0], Snow::Vec3[0, -1, 0], Snow::Vec3[1, 1, 0]])
38
+ #
20
39
  def set_vertices(vertices)
21
- unless vertices.is_a?(Array) && vertices.length >= 3 && vertices.reject { |v| v.is_a?(Vector) && v.size == 3 }.empty?
22
- raise ArgumentError.new("set_vertices() expects an array of at least three 3D Vectors")
23
- end
40
+ type_check(vertices, Array)
41
+ raise ArgumentError.new("set_vertices() expects an array of at least three 3D Vectors") unless vertices.length >= 3
42
+ vertices.each { |v| type_check(v, Snow::Vec3) }
24
43
  @vertices.replace(vertices)
25
44
  end
26
45
 
46
+ ##
47
+ # Returns an array containing all of our local vertices transformed to global-space. (See Actor#get_global_transform.)
48
+ #
27
49
  def get_global_vertices
28
50
  tf = get_global_transform
29
51
  @vertices.map { |v| Transform.transform_point(tf, v) }
30
52
  end
31
53
 
54
+ ##
55
+ # Returns true if the point is inside of this Polygon, false otherwise.
56
+ #
57
+ def is_point_in_bounds(point)
58
+ Collision.is_point_in_shape?(point, self)
59
+ end
60
+
61
+ private
62
+
32
63
  def render(matrix)
64
+ type_check(matrix, Snow::Mat3)
33
65
  global_vertices = @vertices.map { |v| Transform.transform_point(matrix, v) }
34
66
  i = 2
35
67
  while i < global_vertices.length
@@ -44,9 +76,5 @@ module Gosling
44
76
  i += 1
45
77
  end
46
78
  end
47
-
48
- def is_point_in_bounds(point)
49
- Collision.is_point_in_shape?(point, self)
50
- end
51
79
  end
52
80
  end
data/lib/gosling/rect.rb CHANGED
@@ -1,9 +1,16 @@
1
1
  require_relative 'polygon.rb'
2
2
 
3
3
  module Gosling
4
+ ##
5
+ # A Rect is a Polygon with exactly four vertices, defined by a width and height, with sides at right angles to one
6
+ # another. The width and height can be modified at runtime; all vertices will be updated automatically.
7
+ #
4
8
  class Rect < Polygon
5
9
  attr_reader :width, :height
6
10
 
11
+ ##
12
+ # Creates a new Rect with a width and height of 1.
13
+ #
7
14
  def initialize(window)
8
15
  super(window)
9
16
  @width = 1
@@ -23,12 +30,14 @@ module Gosling
23
30
  rebuild_vertices
24
31
  end
25
32
 
33
+ private
34
+
26
35
  def rebuild_vertices
27
36
  vertices = [
28
- Vector[ 0, 0, 0],
29
- Vector[@width, 0, 0],
30
- Vector[@width, @height, 0],
31
- Vector[ 0, @height, 0],
37
+ Snow::Vec3[ 0, 0, 0],
38
+ Snow::Vec3[@width, 0, 0],
39
+ Snow::Vec3[@width, @height, 0],
40
+ Snow::Vec3[ 0, @height, 0],
32
41
  ]
33
42
  set_vertices(vertices)
34
43
  end
@@ -1,6 +1,12 @@
1
1
  require_relative 'rect.rb'
2
2
 
3
3
  module Gosling
4
+ ##
5
+ # This type of Actor accepts a Gosu::Image to be rendered in place of the standard flat-colored shape. It behaves very
6
+ # much like a Rect, except its width and height are automatically set to the width and height of the image given to it
7
+ # and cannot be modified otherwise. The image can be changed at runtime. Changing this actor's color or alpha
8
+ # applies tinting and transparency to the image rendered.
9
+ #
4
10
  class Sprite < Rect
5
11
  def initialize(window)
6
12
  super(window)
@@ -8,10 +14,16 @@ module Gosling
8
14
  @color = Gosu::Color.rgba(255, 255, 255, 255)
9
15
  end
10
16
 
17
+ ##
18
+ # Returns the image currently assigned to this Sprite.
19
+ #
11
20
  def get_image
12
21
  @image
13
22
  end
14
23
 
24
+ ##
25
+ # Assigns the image to this Sprite, setting its width and height to match the image's.
26
+ #
15
27
  def set_image(image)
16
28
  raise ArgumentError.new("Expected Image, but received #{image.inspect}!") unless image.is_a?(Gosu::Image)
17
29
  @image = image
@@ -19,6 +31,8 @@ module Gosling
19
31
  self.height = @image.height
20
32
  end
21
33
 
34
+ private
35
+
22
36
  def render(matrix)
23
37
  global_vertices = @vertices.map { |v| Transform.transform_point(matrix, v) }
24
38
  @image.draw_as_quad(
@@ -1,116 +1,22 @@
1
- require 'matrix'
1
+ require 'snow-math'
2
2
 
3
- module Gosling
4
- class FastMatrix
5
- @@multiply_buffer = []
6
- @@cache = []
7
-
8
- attr_reader :array
9
- attr_accessor :row_count, :column_count
10
-
11
- private
12
-
13
- def initialize
14
- @array = nil
15
- reset
16
- end
17
-
18
- public
19
-
20
- def self.create
21
- if @@cache.empty?
22
- FastMatrix.new
23
- else
24
- @@cache.pop.reset
25
- end
26
- end
27
-
28
- def destroy
29
- reset
30
- @@cache.push(self)
31
- nil
32
- end
33
-
34
- def reset
35
- if @array
36
- @array.clear
37
- else
38
- @array = []
39
- end
40
- @row_count = 0
41
- @column_count = 0
42
- self
43
- end
44
-
45
- def copy_from(matrix)
46
- if matrix.is_a?(Matrix)
47
- @array = matrix.to_a.flatten
48
- @row_count = matrix.row_size
49
- @column_count = matrix.column_size
50
- elsif matrix.is_a?(FastMatrix)
51
- @array = matrix.array
52
- @row_count = matrix.row_count
53
- @column_count = matrix.column_count
54
- else
55
- raise ArgumentError.new("Cannot copy from #{matrix.inspect}!")
56
- end
57
- self
58
- end
59
-
60
- def to_matrix
61
- rows = (0...@row_count).map do |i|
62
- @array[(@column_count * i)...(@column_count * (i + 1))]
63
- end
64
- Matrix.rows(rows)
65
- end
66
-
67
- def fast_multiply(mat2, result = nil)
68
- raise ArgumentError.new() unless mat2.is_a?(FastMatrix) && result.is_a?(FastMatrix)
69
- Matrix.Raise ErrDimensionMismatch if @column_count != mat2.row_count
70
-
71
- i = 0
72
- while i < @row_count do
73
- j = 0
74
- while j < mat2.column_count do
75
- k = 0
76
- sum = 0
77
- while k < @column_count do
78
- sum += @array[i * @column_count + k] * mat2.array[k * mat2.column_count + j]
79
- k += 1
80
- end
81
- @@multiply_buffer[i * @column_count + j] = sum
82
- j += 1
83
- end
84
- i += 1
85
- end
86
-
87
- result ||= FastMatrix.create
88
- result.array.replace(@@multiply_buffer)
89
- result.row_count = @row_count
90
- result.column_count = mat2.column_count
91
- result
92
- end
93
-
94
- def self.combine_matrices(*matrices)
95
- raise ArgumentError.new("Transform.combine_matrices expects one or more matrices") unless matrices.reject { |m| m.is_a?(Matrix) }.empty?
96
-
97
- fast_matrices = matrices.map { |mat| FastMatrix.create.copy_from(mat) }
98
- result = nil
99
- fast_matrices.each do |fast_matrix|
100
- if result
101
- result.fast_multiply(fast_matrix, result)
102
- else
103
- result = fast_matrix
104
- end
105
- end
106
- result
107
- end
108
- end
3
+ require_relative 'patches.rb'
4
+ require_relative 'utils.rb'
109
5
 
6
+ module Gosling
7
+ ##
8
+ # A helper class for performing vector transforms in 2D space. Relies heavily on the Vec3 and Mat3 classes of the
9
+ # SnowMath gem to remain performant.
10
+ #
110
11
  class Transform
12
+ ##
13
+ # Wraps Math.sin to produce rationals instead of floats. Common values are returned quickly from a lookup table.
14
+ #
111
15
  def self.rational_sin(r)
16
+ type_check(r, Numeric)
17
+
112
18
  r = r % (2 * Math::PI)
113
- return case r
19
+ case r
114
20
  when 0.0
115
21
  0.to_r
116
22
  when Math::PI / 2
@@ -124,9 +30,14 @@ module Gosling
124
30
  end
125
31
  end
126
32
 
33
+ ##
34
+ # Wraps Math.cos to produce rationals instead of floats. Common values are returned quickly from a lookup table.
35
+ #
127
36
  def self.rational_cos(r)
37
+ type_check(r, Numeric)
38
+
128
39
  r = r % (2 * Math::PI)
129
- return case r
40
+ case r
130
41
  when 0.0
131
42
  1.to_r
132
43
  when Math::PI / 2
@@ -140,84 +51,274 @@ module Gosling
140
51
  end
141
52
  end
142
53
 
143
- attr_reader :center, :scale, :rotation, :translation
54
+ attr_reader :rotation
144
55
 
56
+ ##
57
+ # Creates a new transform object with no transformations (identity matrix).
145
58
  def initialize
146
- set_center(Vector[0.to_r, 0.to_r, 0.to_r])
147
- set_scale(Vector[1.to_r, 1.to_r])
148
- set_rotation(0)
149
- set_translation(Vector[0.to_r, 0.to_r, 0.to_r])
150
- end
151
-
152
- def set_center(v)
153
- raise ArgumentError.new("Transform.set_center() requires a length 3 vector") unless v.is_a?(Vector) && v.size == 3
154
- @center = Vector[v[0], v[1], 0.to_r]
155
- @center_mat = Matrix[
156
- [1.to_r, 0.to_r, -@center[0].to_r],
157
- [0.to_r, 1.to_r, -@center[1].to_r],
158
- [0.to_r, 0.to_r, 1.to_r]
159
- ]
160
- @is_dirty = true
161
- end
162
-
163
- def set_scale(v)
164
- raise ArgumentError.new("Transform.set_scale() requires a length 2 vector") unless v.is_a?(Vector) && v.size == 2
165
- @scale = v
166
- @scale_mat = Matrix[
167
- [@scale[0].to_r, 0.to_r, 0.to_r],
168
- [0.to_r, @scale[1].to_r, 0.to_r],
169
- [0.to_r, 0.to_r, 1.to_r]
170
- ]
171
- @is_dirty = true
172
- end
173
-
174
- def set_rotation(radians)
59
+ @center = Snow::Vec3[0.to_r, 0.to_r, 1.to_r]
60
+ @scale = Snow::Vec2[1.to_r, 1.to_r]
61
+ @translation = Snow::Vec3[0.to_r, 0.to_r, 1.to_r]
62
+ reset
63
+ end
64
+
65
+ ##
66
+ # Resets center and translation to [0, 0], scale to [1, 1], and rotation to 0, restoring this transformation to the identity
67
+ # matrix.
68
+ #
69
+ def reset
70
+ self.center = 0.to_r, 0.to_r
71
+ self.scale = 1.to_r, 1.to_r
72
+ self.rotation = 0.to_r
73
+ self.translation = 0.to_r, 0.to_r
74
+ end
75
+
76
+ ##
77
+ # Returns a duplicate of the center Vec3 (@center is read-only).
78
+ #
79
+ def center
80
+ @center.dup
81
+ end
82
+
83
+ ##
84
+ # Returns a duplicate of the scale Vec2 (@scale is read-only).
85
+ #
86
+ def scale
87
+ @scale.dup
88
+ end
89
+
90
+ ##
91
+ # Returns a duplicate of the translation Vec3 (@translation is read-only).
92
+ #
93
+ def translation
94
+ @translation.dup
95
+ end
96
+
97
+ ##
98
+ # Sets this transform's centerpoint. All other transforms are performed relative to this central point.
99
+ #
100
+ # The default centerpoint is [0, 0], which is the same as no transform. For a square defined by the vertices
101
+ # [[0, 0], [10, 0], [10, 10], [0, 10]], this would translate to that square's upper left corner. In this case, when scaled
102
+ # larger or smaller, only the square's right and bottom edges would expand or contract, and when rotated it
103
+ # would spin around its upper left corner. For most applications, this is probably not what we want.
104
+ #
105
+ # By setting the centerpoint to something other than the origin, we can change the scaling and rotation to
106
+ # something that makes more sense. For the square defined above, we could set center to be the actual center of
107
+ # the square: [5, 5]. By doing so, scaling the square would cause it to expand evenly on all sides, and rotating it
108
+ # would cause it to spin about its center like a four-cornered wheel.
109
+ #
110
+ # You can set the centerpoint to be any point in local space, inside or even outside of the shape in question.
111
+ #
112
+ # If passed more than two numeric arguments, only the first two are used.
113
+ #
114
+ # Usage:
115
+ # - transform.center = x, y
116
+ # - transform.center = [x, y]
117
+ # - transform.center = Snow::Vec2[x, y]
118
+ # - transform.center = Snow::Vec3[x, y, z]
119
+ # - transform.center = Snow::Vec4[x, y, z, c]
120
+ #
121
+ def center=(args)
122
+ case args[0]
123
+ when Array
124
+ self.center = args[0][0], args[0][1]
125
+ when Snow::Vec2, Snow::Vec3, Snow::Vec4
126
+ @center.x = args[0].x
127
+ @center.y = args[0].y
128
+ when Numeric
129
+ raise ArgumentError.new("Cannot set center from #{args.inspect}: numeric array requires at least two arguments!") unless args.length >= 2
130
+ args.each { |arg| type_check(arg, Numeric) }
131
+ @center.x = args[0]
132
+ @center.y = args[1]
133
+ else
134
+ raise ArgumentError.new("Cannot set center from #{args.inspect}: bad type!")
135
+ end
136
+ @center_is_dirty = @is_dirty = true
137
+ end
138
+ alias :set_center :center=
139
+
140
+ ##
141
+ # Sets this transform's x/y scaling. A scale value of [1, 1] results in no scaling. Larger values make a shape bigger,
142
+ # while smaller values will make it smaller. Great for shrinking/growing animations, or to zoom the camera in/out.
143
+ #
144
+ # If passed more than two numeric arguments, only the first two are used.
145
+ #
146
+ # Usage:
147
+ # - transform.scale = x, y
148
+ # - transform.scale = [x, y]
149
+ # - transform.scale = Snow::Vec2[x, y]
150
+ # - transform.scale = Snow::Vec3[x, y, z]
151
+ # - transform.scale = Snow::Vec4[x, y, z, c]
152
+ #
153
+ def scale=(args)
154
+ case args[0]
155
+ when Array
156
+ self.scale = args[0][0], args[0][1]
157
+ when Snow::Vec2, Snow::Vec3, Snow::Vec4
158
+ @scale.x = args[0].x
159
+ @scale.y = args[0].y
160
+ when Numeric
161
+ raise ArgumentError.new("Cannot set scale from #{args.inspect}: numeric array requires at least two arguments!") unless args.length >= 2
162
+ args.each { |arg| type_check(arg, Numeric) }
163
+ @scale.x = args[0]
164
+ @scale.y = args[1]
165
+ else
166
+ raise ArgumentError.new("Cannot set scale from #{args.inspect}: bad type!")
167
+ end
168
+ @scale_is_dirty = @is_dirty = true
169
+ end
170
+ alias :set_scale :scale=
171
+
172
+ ##
173
+ # Sets this transform's rotation in radians. A value of 0 results in no rotation. Great for spinning animations, or
174
+ # rotating the player's camera view.
175
+ #
176
+ # Usage:
177
+ # - transform.rotation = radians
178
+ #
179
+ def rotation=(radians)
180
+ type_check(radians, Numeric)
175
181
  @rotation = radians
176
- @rotate_mat = Matrix[
177
- [Transform.rational_cos(@rotation), Transform.rational_sin(@rotation), 0.to_r],
178
- [-Transform.rational_sin(@rotation), Transform.rational_cos(@rotation), 0.to_r],
179
- [0.to_r, 0.to_r, 1.to_r]
180
- ]
181
- @is_dirty = true
182
+ @rotate_is_dirty = @is_dirty = true
182
183
  end
184
+ alias :set_rotation :rotation=
183
185
 
184
- def set_translation(v)
185
- raise ArgumentError.new("Transform.set_translation() requires a length 3 vector") unless v.is_a?(Vector) && v.size == 3
186
- @translation = Vector[v[0], v[1], 0.to_r]
187
- @translate_mat = Matrix[
188
- [1.to_r, 0.to_r, @translation[0].to_r],
189
- [0.to_r, 1.to_r, @translation[1].to_r],
190
- [0.to_r, 0.to_r, 1.to_r]
191
- ]
192
- @is_dirty = true
186
+ ##
187
+ # Sets this transform's x/y translation in radians. A value of [0, 0] results in no translation. Great for moving
188
+ # actors across the screen or scrolling the camera.
189
+ #
190
+ # If passed more than two numeric arguments, only the first two are used.
191
+ #
192
+ # Usage:
193
+ # - transform.translation = x, y
194
+ # - transform.translation = [x, y]
195
+ # - transform.translation = Snow::Vec2[x, y]
196
+ # - transform.translation = Snow::Vec3[x, y, z]
197
+ # - transform.translation = Snow::Vec4[x, y, z, c]
198
+ #
199
+ def translation=(args)
200
+ case args[0]
201
+ when Array
202
+ self.translation = args[0][0], args[0][1]
203
+ when Snow::Vec2, Snow::Vec3, Snow::Vec4
204
+ @translation.x = args[0].x
205
+ @translation.y = args[0].y
206
+ when Numeric
207
+ raise ArgumentError.new("Cannot set translation from #{args.inspect}: numeric array requires at least two arguments!") unless args.length >= 2
208
+ args.each { |arg| type_check(arg, Numeric) }
209
+ @translation.x = args[0]
210
+ @translation.y = args[1]
211
+ else
212
+ raise ArgumentError.new("Cannot set translation from #{args.inspect}: bad type!")
213
+ end
214
+ @translate_is_dirty = @is_dirty = true
193
215
  end
216
+ alias :set_translation :translation=
194
217
 
218
+ ##
219
+ # Returns a Snow::Mat3 which combines our current center, scale, rotation, and translation into a single transform
220
+ # matrix. When a point in space is multiplied by this transform, the centering, scaling, rotation, and translation
221
+ # will all be applied to that point.
222
+ #
223
+ # This Snow::Mat3 is cached and will only be recalculated as needed.
224
+ #
195
225
  def to_matrix
196
- return @matrix unless @is_dirty
197
- #~ @matrix = @translate_mat * @rotate_mat * @scale_mat * @center_mat
198
- @matrix = FastMatrix.combine_matrices(@translate_mat, @rotate_mat, @scale_mat, @center_mat).to_matrix
226
+ return @matrix unless @is_dirty || @matrix.nil?
227
+
228
+ update_center_matrix
229
+ update_scale_matrix
230
+ update_rotate_matrix
231
+ update_translate_matrix
232
+
233
+ @matrix = Snow::Mat3.new unless @matrix
234
+
235
+ @matrix.set(@center_mat)
236
+ @matrix.multiply!(@scale_mat)
237
+ @matrix.multiply!(@rotate_mat)
238
+ @matrix.multiply!(@translate_mat)
239
+
199
240
  @is_dirty = false
200
241
  @matrix
201
242
  end
202
243
 
244
+ ##
245
+ # Transforms a Vec3 using the provided Mat3 transform and returns the result as a new Vec3. This is the
246
+ # opposite of Transform.untransform_point.
247
+ #
203
248
  def self.transform_point(mat, v)
204
- raise ArgumentError.new("Transform.transform_point() requires a length 3 vector") unless v.is_a?(Vector) && v.size == 3
205
- result = mat * Vector[v[0], v[1], 1.to_r]
206
- Vector[result[0], result[1], 0.to_r]
249
+ type_check(mat, Snow::Mat3)
250
+ type_check(v, Snow::Vec3)
251
+ result = mat * Snow::Vec3[v[0], v[1], 1.to_r]
252
+ result[2] = 0.to_r
253
+ result
207
254
  end
208
255
 
256
+ ##
257
+ # Applies all of our transformations to the point, returning the resulting point as a new Vec3. This is the opposite
258
+ # of Transform#untransform_point.
259
+ #
209
260
  def transform_point(v)
210
261
  Transform.transform_point(to_matrix, v)
211
262
  end
212
263
 
264
+ ##
265
+ # Transforms a Vec3 using the inverse of the provided Mat3 transform and returns the result as a new Vec3. This
266
+ # is the opposite of Transform.transform_point.
267
+ #
213
268
  def self.untransform_point(mat, v)
214
- raise ArgumentError.new("Transform.transform_point() requires a length 3 vector") unless v.is_a?(Vector) && v.size == 3
215
- result = mat.inverse * Vector[v[0], v[1], 1.to_r]
216
- Vector[result[0], result[1], 0.to_r]
269
+ type_check(mat, Snow::Mat3)
270
+ type_check(v, Snow::Vec3)
271
+ inverse_mat = mat.inverse
272
+ unless inverse_mat
273
+ raise "Unable to invert matrix: #{mat}!"
274
+ end
275
+ result = mat.inverse * Snow::Vec3[v[0], v[1], 1.to_r]
276
+ result[2] = 0.to_r
277
+ result
217
278
  end
218
279
 
280
+ ##
281
+ # Applies the inverse of all of our transformations to the point, returning the resulting point as a new Vec3. This
282
+ # is the opposite of Transform#transform_point.
283
+ #
219
284
  def untransform_point(v)
220
285
  Transform.untransform_point(to_matrix, v)
221
286
  end
287
+
288
+ private
289
+
290
+ def update_center_matrix
291
+ return unless @center_is_dirty || @center_mat.nil?
292
+ @center_mat ||= Snow::Mat3.new
293
+ @center_mat[2] = -@center.x
294
+ @center_mat[5] = -@center.y
295
+ @center_is_dirty = false
296
+ end
297
+
298
+ def update_scale_matrix
299
+ return unless @scale_is_dirty || @scale_mat.nil?
300
+ @scale_mat ||= Snow::Mat3.new
301
+ @scale_mat[0] = @scale[0].to_r
302
+ @scale_mat[4] = @scale[1].to_r
303
+ @scale_is_dirty = false
304
+ end
305
+
306
+ def update_rotate_matrix
307
+ return unless @rotate_is_dirty || @rotate_mat.nil?
308
+ @rotate_mat ||= Snow::Mat3.new
309
+ @rotate_mat[0] = Transform.rational_cos(@rotation)
310
+ @rotate_mat[1] = Transform.rational_sin(@rotation)
311
+ @rotate_mat[3] = -Transform.rational_sin(@rotation)
312
+ @rotate_mat[4] = Transform.rational_cos(@rotation)
313
+ @rotate_is_dirty = false
314
+ end
315
+
316
+ def update_translate_matrix
317
+ return unless @translate_is_dirty || @translate_mat.nil?
318
+ @translate_mat ||= Snow::Mat3.new
319
+ @translate_mat[2] = @translation.x
320
+ @translate_mat[5] = @translation.y
321
+ @translate_is_dirty = false
322
+ end
222
323
  end
223
324
  end