vector2d 2.3.0 → 3.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/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +1 -1
- data/.yardopts +11 -0
- data/CHANGELOG.md +216 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +35 -5
- data/README.md +325 -12
- data/Rakefile +18 -0
- data/benchmark/vector_comparison.rb +129 -0
- data/lib/vector2d/angles.rb +315 -0
- data/lib/vector2d/arithmetic.rb +97 -0
- data/lib/vector2d/comparison.rb +188 -0
- data/lib/vector2d/componentwise.rb +239 -0
- data/lib/vector2d/constructors.rb +137 -0
- data/lib/vector2d/conversions.rb +139 -0
- data/lib/vector2d/coordinates.rb +22 -0
- data/lib/vector2d/deprecation.rb +16 -0
- data/lib/vector2d/dimensions.rb +297 -0
- data/lib/vector2d/interpolation.rb +191 -0
- data/lib/vector2d/lengths.rb +284 -0
- data/lib/vector2d/matrix_interop.rb +93 -0
- data/lib/vector2d/parsing.rb +142 -0
- data/lib/vector2d/projection.rb +208 -0
- data/lib/vector2d/version.rb +2 -1
- data/lib/vector2d.rb +172 -60
- data/spec/lib/vector2d/angles_spec.rb +441 -0
- data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
- data/spec/lib/vector2d/comparison_spec.rb +358 -0
- data/spec/lib/vector2d/componentwise_spec.rb +300 -0
- data/spec/lib/vector2d/constructors_spec.rb +299 -0
- data/spec/lib/vector2d/conversions_spec.rb +234 -0
- data/spec/lib/vector2d/dimensions_spec.rb +820 -0
- data/spec/lib/vector2d/interpolation_spec.rb +322 -0
- data/spec/lib/vector2d/lengths_spec.rb +457 -0
- data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
- data/spec/lib/vector2d/parsing_spec.rb +329 -0
- data/spec/lib/vector2d/projection_spec.rb +306 -0
- data/spec/lib/vector2d_documentation_spec.rb +38 -0
- data/spec/lib/vector2d_immutability_spec.rb +263 -0
- data/spec/lib/vector2d_spec.rb +90 -50
- data/spec/lib/vector2d_subclassing_spec.rb +210 -0
- data/spec/lib/vector2d_tags_spec.rb +43 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/doc_examples/comments.rb +65 -0
- data/spec/support/doc_examples/markdown.rb +50 -0
- data/spec/support/doc_examples.rb +94 -0
- data/spec/support/doc_tags.rb +151 -0
- data/spec/support/shared_examples/class_preserving_method.rb +10 -0
- data/spec/support/shared_examples/deprecated_method.rb +25 -0
- data/spec/support/shared_examples/parsed_vector.rb +11 -0
- data/vector2d.gemspec +2 -1
- metadata +42 -13
- data/.travis.yml +0 -10
- data/lib/vector2d/calculations.rb +0 -141
- data/lib/vector2d/coercions.rb +0 -64
- data/lib/vector2d/fitting.rb +0 -60
- data/lib/vector2d/properties.rb +0 -46
- data/lib/vector2d/transformations.rb +0 -98
- data/spec/lib/vector2d/coercions_spec.rb +0 -59
- data/spec/lib/vector2d/fitting_spec.rb +0 -70
- data/spec/lib/vector2d/properties_spec.rb +0 -47
- data/spec/lib/vector2d/transformations_spec.rb +0 -139
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# The vector as a rectangle. Vector2d grew out of handling image
|
|
5
|
+
# dimensions, and these methods describe a size and fit one inside
|
|
6
|
+
# another. All of them compare coordinates by magnitude, so a
|
|
7
|
+
# negative vector describes the same rectangle as its positive twin.
|
|
8
|
+
module Dimensions
|
|
9
|
+
# Area covered by the vector, the product of its coordinates.
|
|
10
|
+
# Coordinates are taken by magnitude, as in #aspect_ratio, so the
|
|
11
|
+
# area is never negative.
|
|
12
|
+
#
|
|
13
|
+
# Vector2d(2, 3).area # => 6
|
|
14
|
+
# Vector2d(-2, 3).area # => 6
|
|
15
|
+
#
|
|
16
|
+
# A vector without width or height covers nothing.
|
|
17
|
+
#
|
|
18
|
+
# Vector2d(2, 0).area # => 0
|
|
19
|
+
#
|
|
20
|
+
# @return [Integer, Float, Rational, BigDecimal]
|
|
21
|
+
def area
|
|
22
|
+
(x * y).abs
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Aspect ratio of vector.
|
|
26
|
+
#
|
|
27
|
+
# Vector2d(2, 3).aspect_ratio # => 0.6666..
|
|
28
|
+
#
|
|
29
|
+
# A vector without height has no aspect ratio, so ArgumentError is
|
|
30
|
+
# raised.
|
|
31
|
+
#
|
|
32
|
+
# Vector2d(2, 0).aspect_ratio # => ArgumentError
|
|
33
|
+
# Vector2d(0, 0).aspect_ratio # => ArgumentError
|
|
34
|
+
#
|
|
35
|
+
# @return [Float]
|
|
36
|
+
def aspect_ratio
|
|
37
|
+
raise ArgumentError, "the zero vector has no aspect ratio" if zero?
|
|
38
|
+
raise ArgumentError, "#{inspect} has no aspect ratio, y is zero" if y.zero?
|
|
39
|
+
|
|
40
|
+
(x.to_f / y).abs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Is the vector wider than it is tall?
|
|
44
|
+
#
|
|
45
|
+
# Vector2d(3, 2).landscape? # => true
|
|
46
|
+
# Vector2d(2, 3).landscape? # => false
|
|
47
|
+
# Vector2d(2, 2).landscape? # => false
|
|
48
|
+
#
|
|
49
|
+
# Coordinates are compared by magnitude, as in #aspect_ratio.
|
|
50
|
+
#
|
|
51
|
+
# Vector2d(-3, 2).landscape? # => true
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def landscape?
|
|
55
|
+
x.abs > y.abs
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Is the vector taller than it is wide?
|
|
59
|
+
#
|
|
60
|
+
# Vector2d(2, 3).portrait? # => true
|
|
61
|
+
# Vector2d(3, 2).portrait? # => false
|
|
62
|
+
# Vector2d(2, 2).portrait? # => false
|
|
63
|
+
#
|
|
64
|
+
# Coordinates are compared by magnitude, as in #aspect_ratio.
|
|
65
|
+
#
|
|
66
|
+
# Vector2d(2, -3).portrait? # => true
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def portrait?
|
|
70
|
+
x.abs < y.abs
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Is the vector as wide as it is tall?
|
|
74
|
+
#
|
|
75
|
+
# Vector2d(2, 2).square? # => true
|
|
76
|
+
# Vector2d(2, 3).square? # => false
|
|
77
|
+
#
|
|
78
|
+
# Coordinates are compared by magnitude, as in #aspect_ratio.
|
|
79
|
+
# Exactly one of #square?, #landscape? and #portrait? holds for any
|
|
80
|
+
# vector.
|
|
81
|
+
#
|
|
82
|
+
# Vector2d(-2, 2).square? # => true
|
|
83
|
+
#
|
|
84
|
+
# Unlike #aspect_ratio, these three don't single out the zero
|
|
85
|
+
# vector. It is square.
|
|
86
|
+
#
|
|
87
|
+
# Vector2d(0, 0).square? # => true
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def square?
|
|
91
|
+
x.abs == y.abs
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Scales the vector to fit inside another vector, retaining the
|
|
95
|
+
# aspect ratio.
|
|
96
|
+
#
|
|
97
|
+
# vector = Vector2d(20, 10)
|
|
98
|
+
# vector.fit(Vector2d(10, 10)) # => Vector2d(10.0,5.0)
|
|
99
|
+
# vector.fit(Vector2d(20, 20)) # => Vector2d(20.0,10.0)
|
|
100
|
+
# vector.fit(Vector2d(40, 40)) # => Vector2d(40.0,20.0)
|
|
101
|
+
#
|
|
102
|
+
# Pass <tt>upscale: false</tt> to scale down only, leaving a vector
|
|
103
|
+
# that already fits unchanged.
|
|
104
|
+
#
|
|
105
|
+
# vector.fit(Vector2d(40, 40), upscale: false) # => Vector2d(20,10)
|
|
106
|
+
# vector.fit(Vector2d(10, 10), upscale: false) # => Vector2d(10.0,5.0)
|
|
107
|
+
#
|
|
108
|
+
# The constraint applies to the magnitude of each coordinate, and
|
|
109
|
+
# the vector keeps its direction.
|
|
110
|
+
#
|
|
111
|
+
# Vector2d(-20, 10).fit(Vector2d(5, 5)) # => Vector2d(-5.0,2.5)
|
|
112
|
+
#
|
|
113
|
+
# Note: Either axis will be disregarded if zero or nil. This is a
|
|
114
|
+
# feature, not a bug. A constraint that is zero on both axes leaves
|
|
115
|
+
# the vector unchanged.
|
|
116
|
+
#
|
|
117
|
+
# Vector2d(20, 10).fit(Vector2d(0, 0)) # => Vector2d(20,10)
|
|
118
|
+
#
|
|
119
|
+
# The zero vector has no direction, and is returned unchanged.
|
|
120
|
+
#
|
|
121
|
+
# Vector2d(0, 0).fit(Vector2d(10, 10)) # => Vector2d(0,0)
|
|
122
|
+
#
|
|
123
|
+
# @!macro coercible
|
|
124
|
+
# @param upscale [Boolean] whether to scale a vector that already
|
|
125
|
+
# fits up to the constraint
|
|
126
|
+
# @return [self]
|
|
127
|
+
def fit(other, upscale: true)
|
|
128
|
+
scale_by(fit_factors(coerce_vector(other)).min, upscale: upscale)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Does the vector already fit inside another vector? True whenever
|
|
132
|
+
# <tt>fit(other, upscale: false)</tt> would leave it unchanged.
|
|
133
|
+
#
|
|
134
|
+
# constraint = Vector2d(20, 20)
|
|
135
|
+
# Vector2d(20, 10).fits?(constraint) # => true
|
|
136
|
+
# Vector2d(40, 10).fits?(constraint) # => false
|
|
137
|
+
#
|
|
138
|
+
# A vector that exactly matches the constraint fits.
|
|
139
|
+
#
|
|
140
|
+
# Vector2d(20, 20).fits?(constraint) # => true
|
|
141
|
+
#
|
|
142
|
+
# Coordinates are compared by magnitude, as in #fit.
|
|
143
|
+
#
|
|
144
|
+
# Vector2d(-20, 10).fits?(constraint) # => true
|
|
145
|
+
#
|
|
146
|
+
# Note: Either axis will be disregarded if zero or nil, as in #fit.
|
|
147
|
+
# An axis that doesn't constrain can't be overflowed either.
|
|
148
|
+
#
|
|
149
|
+
# Vector2d(40, 10).fits?(Vector2d(0, 20)) # => true
|
|
150
|
+
#
|
|
151
|
+
# The zero vector has no size, and fits inside anything.
|
|
152
|
+
#
|
|
153
|
+
# Vector2d(0, 0).fits?(constraint) # => true
|
|
154
|
+
#
|
|
155
|
+
# @!macro coercible
|
|
156
|
+
# @return [Boolean]
|
|
157
|
+
def fits?(other)
|
|
158
|
+
factor = fit_factors(coerce_vector(other)).min
|
|
159
|
+
factor.nil? || factor >= 1
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Scales the vector to cover another vector, retaining the aspect
|
|
163
|
+
# ratio. Where #fit scales until the vector is contained by the
|
|
164
|
+
# constraint, #cover scales until it contains the constraint.
|
|
165
|
+
#
|
|
166
|
+
# constraint = Vector2d(5, 5)
|
|
167
|
+
# Vector2d(20, 10).cover(constraint) # => Vector2d(10.0,5.0)
|
|
168
|
+
# Vector2d(10, 20).cover(constraint) # => Vector2d(5.0,10.0)
|
|
169
|
+
#
|
|
170
|
+
# Pass <tt>upscale: false</tt> to scale down only, leaving a vector
|
|
171
|
+
# that already covers the constraint unchanged.
|
|
172
|
+
#
|
|
173
|
+
# Vector2d(20, 10).cover(Vector2d(40, 40), upscale: false)
|
|
174
|
+
# # => Vector2d(20,10)
|
|
175
|
+
#
|
|
176
|
+
# As in #fit, coordinates are constrained by magnitude and the
|
|
177
|
+
# vector keeps its direction.
|
|
178
|
+
#
|
|
179
|
+
# Vector2d(-20, 10).cover(constraint) # => Vector2d(-10.0,5.0)
|
|
180
|
+
#
|
|
181
|
+
# Note: Either axis will be disregarded if zero or nil, as in #fit.
|
|
182
|
+
# This is a feature, not a bug.
|
|
183
|
+
#
|
|
184
|
+
# Vector2d(0, 10).cover(constraint) # => Vector2d(0.0,5.0)
|
|
185
|
+
# Vector2d(20, 10).cover(Vector2d(0, 0)) # => Vector2d(20,10)
|
|
186
|
+
#
|
|
187
|
+
# The zero vector has no direction, and is returned unchanged.
|
|
188
|
+
#
|
|
189
|
+
# Vector2d(0, 0).cover(Vector2d(5, 5)) # => Vector2d(0,0)
|
|
190
|
+
#
|
|
191
|
+
# @!macro coercible
|
|
192
|
+
# @param upscale [Boolean] whether to scale a vector that already
|
|
193
|
+
# covers up to the constraint
|
|
194
|
+
# @return [self]
|
|
195
|
+
def cover(other, upscale: true)
|
|
196
|
+
scale_by(fit_factors(coerce_vector(other)).max, upscale: upscale)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Does the vector already cover another vector? True whenever
|
|
200
|
+
# #cover would shrink the vector or leave it alone, rather than
|
|
201
|
+
# scaling it up. This is the predicate to #cover that #fits? is to
|
|
202
|
+
# #fit.
|
|
203
|
+
#
|
|
204
|
+
# constraint = Vector2d(5, 5)
|
|
205
|
+
# Vector2d(20, 10).covers?(constraint) # => true
|
|
206
|
+
# Vector2d(20, 1).covers?(constraint) # => false
|
|
207
|
+
#
|
|
208
|
+
# Coordinates are compared by magnitude, as in #cover.
|
|
209
|
+
#
|
|
210
|
+
# Vector2d(-20, 10).covers?(constraint) # => true
|
|
211
|
+
#
|
|
212
|
+
# Note: Either axis will be disregarded if zero or nil, as in
|
|
213
|
+
# #cover, so a vector flat on one axis still counts as covering.
|
|
214
|
+
#
|
|
215
|
+
# Vector2d(0, 10).covers?(constraint) # => true
|
|
216
|
+
# Vector2d(0, 0).covers?(constraint) # => true
|
|
217
|
+
#
|
|
218
|
+
# @!macro coercible
|
|
219
|
+
# @return [Boolean]
|
|
220
|
+
def covers?(other)
|
|
221
|
+
factor = fit_factors(coerce_vector(other)).max
|
|
222
|
+
factor.nil? || factor <= 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @deprecated Use #cover instead.
|
|
226
|
+
#
|
|
227
|
+
# @!macro coercible
|
|
228
|
+
# @return [self]
|
|
229
|
+
def fit_either(other)
|
|
230
|
+
warn_deprecated("Vector2d#fit_either is deprecated. Use #cover instead.")
|
|
231
|
+
cover(other)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @deprecated Use <tt>other.fit(self, upscale: false)</tt> instead.
|
|
235
|
+
#
|
|
236
|
+
# Scales down the given vector unless it fits inside.
|
|
237
|
+
#
|
|
238
|
+
# vector = Vector2d(20, 20)
|
|
239
|
+
# vector.contain(Vector2d(10, 10)) # => Vector2d(10,10)
|
|
240
|
+
# vector.contain(Vector2d(40, 20)) # => Vector2d(20.0,10.0)
|
|
241
|
+
# vector.contain(Vector2d(20, 40)) # => Vector2d(10.0,20.0)
|
|
242
|
+
#
|
|
243
|
+
# Coordinates are compared by magnitude, so negative vectors are
|
|
244
|
+
# scaled the same way and keep their direction.
|
|
245
|
+
#
|
|
246
|
+
# vector.contain(Vector2d(-40, 20)) # => Vector2d(-20.0,10.0)
|
|
247
|
+
#
|
|
248
|
+
# An axis that is zero is unconstrained, as in #fit, so the zero
|
|
249
|
+
# vector contains anything.
|
|
250
|
+
#
|
|
251
|
+
# Vector2d(0, 0).contain(Vector2d(40, 20)) # => Vector2d(40,20)
|
|
252
|
+
#
|
|
253
|
+
# @!macro coercible
|
|
254
|
+
# @return [Vector2d] a vector of the argument's class, not of this one
|
|
255
|
+
def contain(other)
|
|
256
|
+
warn_deprecated("Vector2d#contain is deprecated. " \
|
|
257
|
+
"Use `other.fit(self, upscale: false)` instead.")
|
|
258
|
+
coerce_vector(other).fit(self, upscale: false)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# @deprecated Use #fit instead.
|
|
262
|
+
#
|
|
263
|
+
# @!macro coercible
|
|
264
|
+
# @return [self]
|
|
265
|
+
def constrain_both(other)
|
|
266
|
+
warn_deprecated("Vector2d#constrain_both is deprecated. Use #fit instead.")
|
|
267
|
+
fit(other)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @deprecated Use #cover instead.
|
|
271
|
+
#
|
|
272
|
+
# @!macro coercible
|
|
273
|
+
# @return [self]
|
|
274
|
+
def constrain_one(other)
|
|
275
|
+
warn_deprecated("Vector2d#constrain_one is deprecated. Use #cover instead.")
|
|
276
|
+
cover(other)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
protected
|
|
280
|
+
|
|
281
|
+
# Magnitudes of the scale factor for each axis, disregarding axes
|
|
282
|
+
# that don't constrain the vector.
|
|
283
|
+
def fit_factors(other)
|
|
284
|
+
scale = other.to_f_vector / self
|
|
285
|
+
scale.to_a.select { |s| s.finite? && !s.zero? }.map(&:abs)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Scales the vector by the given factor. An unconstrained vector,
|
|
289
|
+
# which has no factor, is returned unchanged, as is one that would
|
|
290
|
+
# grow when +upscale+ is false.
|
|
291
|
+
def scale_by(factor, upscale:)
|
|
292
|
+
return self if factor.nil? || (!upscale && factor >= 1)
|
|
293
|
+
|
|
294
|
+
self * factor
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Vector2d
|
|
4
|
+
# Interpolation between two vectors, and stepping from one toward
|
|
5
|
+
# another.
|
|
6
|
+
module Interpolation
|
|
7
|
+
# Linearly interpolates between this vector and another vector.
|
|
8
|
+
#
|
|
9
|
+
# v1 = Vector2d(0, 0)
|
|
10
|
+
# v2 = Vector2d(10, 20)
|
|
11
|
+
# v1.lerp(v2, 0.0) # => Vector2d(0.0,0.0)
|
|
12
|
+
# v1.lerp(v2, 0.25) # => Vector2d(2.5,5.0)
|
|
13
|
+
# v1.lerp(v2, 1.0) # => Vector2d(10.0,20.0)
|
|
14
|
+
#
|
|
15
|
+
# The amount is not clamped to 0..1. Values outside that range
|
|
16
|
+
# extrapolate past the end points.
|
|
17
|
+
#
|
|
18
|
+
# v1.lerp(v2, 2.0) # => Vector2d(20.0,40.0)
|
|
19
|
+
# v1.lerp(v2, -0.5) # => Vector2d(-5.0,-10.0)
|
|
20
|
+
#
|
|
21
|
+
# Raises ArgumentError unless the amount is a real number. One
|
|
22
|
+
# amount applies to both axes.
|
|
23
|
+
#
|
|
24
|
+
# v1.lerp(v2, Vector2d(0.25, 0.5)) # => ArgumentError
|
|
25
|
+
#
|
|
26
|
+
# @!macro coercible
|
|
27
|
+
# @param amount [Integer, Float, Rational, BigDecimal]
|
|
28
|
+
# the position along the segment
|
|
29
|
+
# @return [self]
|
|
30
|
+
def lerp(other, amount)
|
|
31
|
+
v = coerce_vector(other)
|
|
32
|
+
amount = coordinate(amount)
|
|
33
|
+
build(interpolate(x, v.x, amount), interpolate(y, v.y, amount))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the amount #lerp would need to land on a value, the
|
|
37
|
+
# position of that value along the segment from this vector to
|
|
38
|
+
# another one. This is the inverse of #lerp.
|
|
39
|
+
#
|
|
40
|
+
# v1 = Vector2d(0, 0)
|
|
41
|
+
# v2 = Vector2d(10, 20)
|
|
42
|
+
# v1.inverse_lerp(v2, Vector2d(2.5, 5.0)) # => 0.25
|
|
43
|
+
# v1.inverse_lerp(v2, v1) # => 0.0
|
|
44
|
+
# v1.inverse_lerp(v2, v2) # => 1.0
|
|
45
|
+
#
|
|
46
|
+
# The result is a scalar, one amount for both axes, matching the
|
|
47
|
+
# single amount #lerp takes.
|
|
48
|
+
#
|
|
49
|
+
# v1.lerp(v2, v1.inverse_lerp(v2, Vector2d(2.5, 5.0))) # => Vector2d(2.5,5.0)
|
|
50
|
+
#
|
|
51
|
+
# The value does not have to lie on the segment. Anything off it is
|
|
52
|
+
# projected onto the line through the end points first, so the
|
|
53
|
+
# result is the position of the nearest point on that line.
|
|
54
|
+
#
|
|
55
|
+
# v1.inverse_lerp(v2, Vector2d(5, 0)) # => 0.1
|
|
56
|
+
#
|
|
57
|
+
# The result is not clamped to 0..1, the same way the amount #lerp
|
|
58
|
+
# takes is not. Values beyond the end points fall outside it.
|
|
59
|
+
#
|
|
60
|
+
# v1.inverse_lerp(v2, Vector2d(20, 40)) # => 2.0
|
|
61
|
+
# v1.inverse_lerp(v2, Vector2d(-5, -10)) # => -0.5
|
|
62
|
+
#
|
|
63
|
+
# A segment between two identical vectors has no length to measure
|
|
64
|
+
# along, and no amount reaches anything but its own end point. Zero
|
|
65
|
+
# is returned.
|
|
66
|
+
#
|
|
67
|
+
# v1.inverse_lerp(v1, Vector2d(2.5, 5.0)) # => 0.0
|
|
68
|
+
#
|
|
69
|
+
# @!macro coercible
|
|
70
|
+
# @param value [Vector2d, Array, String, Hash, Integer, Float, Rational,
|
|
71
|
+
# BigDecimal, ::Vector, ::Matrix] the value to locate, anything
|
|
72
|
+
# Vector2d.parse accepts
|
|
73
|
+
# @return [Float] the amount #lerp would take to reach the value
|
|
74
|
+
def inverse_lerp(other, value)
|
|
75
|
+
segment = coerce_vector(other) - self
|
|
76
|
+
return 0.0 if segment.zero?
|
|
77
|
+
|
|
78
|
+
(coerce_vector(value) - self).dot_product(segment).to_f / segment.length_squared
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Spherically interpolates between this vector and another vector.
|
|
82
|
+
# The vector turns through the angle between the two, while the
|
|
83
|
+
# length is interpolated linearly. The other vector is coerced, so
|
|
84
|
+
# scalars work too.
|
|
85
|
+
#
|
|
86
|
+
# v1 = Vector2d(2, 0)
|
|
87
|
+
# v2 = Vector2d(0, 4)
|
|
88
|
+
# v1.slerp(v2, 0.5) # => Vector2d(2.1213..,2.1213..)
|
|
89
|
+
#
|
|
90
|
+
# Where #lerp moves along the straight line between the two
|
|
91
|
+
# vectors, #slerp moves along the arc between them, so the length
|
|
92
|
+
# follows the end points instead of cutting the corner.
|
|
93
|
+
#
|
|
94
|
+
# v1.lerp(v2, 0.5).length # => 2.2360..
|
|
95
|
+
# v1.slerp(v2, 0.5).length # => 3.0
|
|
96
|
+
#
|
|
97
|
+
# Vectors pointing in opposite directions are half a turn apart
|
|
98
|
+
# either way around. The turn is counterclockwise, the direction
|
|
99
|
+
# #rotate takes a positive angle in.
|
|
100
|
+
#
|
|
101
|
+
# half = Vector2d(2, 0).slerp(Vector2d(-2, 0), 0.5)
|
|
102
|
+
# half.angle # => 1.5707..
|
|
103
|
+
# half.length # => 2.0
|
|
104
|
+
#
|
|
105
|
+
# The zero vector has no direction to turn from or to, and there is
|
|
106
|
+
# no arc to follow. #lerp is used instead.
|
|
107
|
+
#
|
|
108
|
+
# Vector2d(0, 0).slerp(v2, 0.5) # => Vector2d(0.0,2.0)
|
|
109
|
+
#
|
|
110
|
+
# The amount is not clamped to 0..1, and behaves as it does in
|
|
111
|
+
# #lerp. Values outside that range keep turning past the end
|
|
112
|
+
# points.
|
|
113
|
+
#
|
|
114
|
+
# v1.slerp(v2, 2.0).angle # => 3.1415..
|
|
115
|
+
#
|
|
116
|
+
# @!macro coercible
|
|
117
|
+
# @param amount [Integer, Float, Rational, BigDecimal]
|
|
118
|
+
# the position along the segment
|
|
119
|
+
# @return [self]
|
|
120
|
+
def slerp(other, amount)
|
|
121
|
+
v = coerce_vector(other)
|
|
122
|
+
amount = coordinate(amount)
|
|
123
|
+
return lerp(v, amount) if zero? || v.zero?
|
|
124
|
+
|
|
125
|
+
rotate(slerp_angle(v) * amount) *
|
|
126
|
+
(interpolate(length, v.length, amount) / length)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns the point halfway between this vector and another vector.
|
|
130
|
+
#
|
|
131
|
+
# v1 = Vector2d(0, 0)
|
|
132
|
+
# v2 = Vector2d(10, 20)
|
|
133
|
+
# v1.midpoint(v2) # => Vector2d(5.0,10.0)
|
|
134
|
+
#
|
|
135
|
+
# @!macro coercible
|
|
136
|
+
# @return [self]
|
|
137
|
+
def midpoint(other)
|
|
138
|
+
lerp(other, 0.5)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Moves this vector toward another vector by a fixed distance,
|
|
142
|
+
# stopping at the target instead of overshooting it. The target is
|
|
143
|
+
# coerced, so scalars work too.
|
|
144
|
+
#
|
|
145
|
+
# v1 = Vector2d(0, 0)
|
|
146
|
+
# v2 = Vector2d(10, 0)
|
|
147
|
+
# v1.move_toward(v2, 2) # => Vector2d(2.0,0.0)
|
|
148
|
+
# v1.move_toward(v2, 20) # => Vector2d(10,0)
|
|
149
|
+
#
|
|
150
|
+
# Where #lerp takes a fraction of the way there, this takes a
|
|
151
|
+
# distance, so the step is the same size however far off the target
|
|
152
|
+
# is.
|
|
153
|
+
#
|
|
154
|
+
# A negative distance moves away from the target, and has nothing
|
|
155
|
+
# to overshoot.
|
|
156
|
+
#
|
|
157
|
+
# v1.move_toward(v2, -2) # => Vector2d(-2.0,0.0)
|
|
158
|
+
#
|
|
159
|
+
# There is nowhere to move when the vector is already at the
|
|
160
|
+
# target, whatever the distance. The target is returned.
|
|
161
|
+
#
|
|
162
|
+
# v2.move_toward(v2, 2) # => Vector2d(10,0)
|
|
163
|
+
#
|
|
164
|
+
# @!macro coercible
|
|
165
|
+
# @param distance [Integer, Float, Rational, BigDecimal] how far to move
|
|
166
|
+
# @return [self]
|
|
167
|
+
def move_toward(target, distance)
|
|
168
|
+
v = coerce_vector(target)
|
|
169
|
+
distance = coordinate(distance)
|
|
170
|
+
delta = build(v.x - x, v.y - y)
|
|
171
|
+
return build(v.x, v.y) if delta.zero? || distance >= delta.length
|
|
172
|
+
|
|
173
|
+
self + delta.resize(distance)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# The angle #slerp turns through. Vectors pointing in opposite
|
|
179
|
+
# directions are half a turn apart either way around, and the
|
|
180
|
+
# counterclockwise one is taken.
|
|
181
|
+
def slerp_angle(other)
|
|
182
|
+
return Math::PI if parallel?(other) && dot_product(other).negative?
|
|
183
|
+
|
|
184
|
+
angle_to(other)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def interpolate(start, finish, amount)
|
|
188
|
+
start + ((finish - start) * amount)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|