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.
- 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
|