gosling 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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