gosling 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/gosling/actor.rb +234 -54
- data/lib/gosling/circle.rb +27 -5
- data/lib/gosling/collision.rb +55 -20
- data/lib/gosling/image_library.rb +7 -0
- data/lib/gosling/patches.rb +42 -0
- data/lib/gosling/polygon.rb +39 -11
- data/lib/gosling/rect.rb +13 -4
- data/lib/gosling/sprite.rb +14 -0
- data/lib/gosling/transform.rb +264 -163
- data/lib/gosling/utils.rb +20 -0
- data/lib/gosling/version.rb +5 -1
- data/spec/actor_spec.rb +47 -44
- data/spec/circle_spec.rb +2 -3
- data/spec/collision_spec.rb +207 -206
- data/spec/polygon_spec.rb +36 -36
- data/spec/rect_spec.rb +12 -12
- data/spec/transform_spec.rb +138 -149
- metadata +18 -2
data/lib/gosling/collision.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
102
|
-
|
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 *
|
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.
|
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
|
-
|
119
|
-
|
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
|
data/lib/gosling/polygon.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
data/lib/gosling/sprite.rb
CHANGED
@@ -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(
|
data/lib/gosling/transform.rb
CHANGED
@@ -1,116 +1,22 @@
|
|
1
|
-
require '
|
1
|
+
require 'snow-math'
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
-
|
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
|
-
|
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 :
|
54
|
+
attr_reader :rotation
|
144
55
|
|
56
|
+
##
|
57
|
+
# Creates a new transform object with no transformations (identity matrix).
|
145
58
|
def initialize
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
end
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
@
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
|
-
|
198
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
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
|