gifenc 0.1.0 → 0.2.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/.yardopts +1 -1
- data/CHANGELOG.md +36 -0
- data/README.md +7 -2
- data/lib/errors.rb +4 -0
- data/lib/extensions.rb +11 -11
- data/lib/geometry.rb +599 -23
- data/lib/gif.rb +47 -2
- data/lib/gifenc.rb +7 -0
- data/lib/image.rb +808 -115
- data/lib/util.rb +10 -0
- metadata +11 -8
data/lib/geometry.rb
CHANGED
|
@@ -2,8 +2,472 @@ module Gifenc
|
|
|
2
2
|
# This module encapsulates all the necessary geometric functionality, and
|
|
3
3
|
# more generally, all mathematical methods that may be useful for several
|
|
4
4
|
# tasks of the library, such as drawing, resampling, etc.
|
|
5
|
+
#
|
|
6
|
+
# Every method that takes a point as argument may be supplied by providing
|
|
7
|
+
# either a `Point` or a `[Float, Float]` array representing its coordinates,
|
|
8
|
+
# regardless of whether the documentation has one or the other in the method's
|
|
9
|
+
# specification.
|
|
5
10
|
module Geometry
|
|
6
11
|
|
|
12
|
+
# Represents a point in the plane. It's essentially a wrapper for an Float
|
|
13
|
+
# array with 2 elements (the coordinates) and many geometric methods that
|
|
14
|
+
# aid working with them. It is used indistinctly for both points and vectors,
|
|
15
|
+
# and will be denoted as such throughout the code, depending on which
|
|
16
|
+
# interpretation is more relevant.
|
|
17
|
+
class Point
|
|
18
|
+
|
|
19
|
+
# The X coordinate of the point.
|
|
20
|
+
# @return [Integer] X coordinate.
|
|
21
|
+
attr_accessor :x
|
|
22
|
+
|
|
23
|
+
# The Y coordinate of the point.
|
|
24
|
+
# @return [Integer] Y coordinate.
|
|
25
|
+
attr_accessor :y
|
|
26
|
+
|
|
27
|
+
# Convert polar coordinates to rectangular (Cartesian) coordinates.
|
|
28
|
+
# @param mod [Float] The point's module (euclidean norm).
|
|
29
|
+
# @param arg [Float] The point's argument (angle with respect to the
|
|
30
|
+
# positive X axis).
|
|
31
|
+
# @return [Array<Float>] The corresponding Cartesian coordinates.
|
|
32
|
+
def self.polar2rect(mod, arg)
|
|
33
|
+
[mod * Math.cos(arg), mod * Math.sin(arg)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert rectangular (Cartesian) coordinates to polar coordinates.
|
|
37
|
+
# @param x [Float] The point's X coordinate.
|
|
38
|
+
# @param y [Float] The point's Y coordinate.
|
|
39
|
+
# @return [Array<Float>] The corresponding polar coordinates.
|
|
40
|
+
def self.rect2polar(x, y)
|
|
41
|
+
[(x ** 2 + y ** 2) ** 0.5, Math.atan2(y, x)]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parse a point from an arbitrary argument. It accepts either:
|
|
45
|
+
# * A point object, in which case it returns itself.
|
|
46
|
+
# * An array, in which case it creates a new point whose coordinates are
|
|
47
|
+
# the values of the array.
|
|
48
|
+
# @param point [Point,Array<Integer>] The parameter to parse the point from.
|
|
49
|
+
# @param sys [Symbol] The coordinate system to use for parsing the
|
|
50
|
+
# coordinates. It may be `:cartesian` or `:polar`.
|
|
51
|
+
# @return [Point] The parsed point object.
|
|
52
|
+
# @raise [Exception::GeometryError] When a point couldn't be parsed from the supplied
|
|
53
|
+
# argument.
|
|
54
|
+
def self.parse(point, sys = :cartesian)
|
|
55
|
+
if point.is_a?(Point)
|
|
56
|
+
point
|
|
57
|
+
elsif point.is_a?(Array)
|
|
58
|
+
point = polar2rect(*point) if sys == :polar
|
|
59
|
+
new(*point)
|
|
60
|
+
else
|
|
61
|
+
raise Exception::GeometryError, "Couldn't parse point from argument."
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Create a new point given its coordinates. The coordinates are assumed to
|
|
66
|
+
# be the Cartesian coordinates with respect to the same axes as the desired
|
|
67
|
+
# image, and they need not be integers (though they'll be casted as such
|
|
68
|
+
# when actually drawing them).
|
|
69
|
+
# @param x [Float] The X coordinate of the point.
|
|
70
|
+
# @param y [Float] The Y coordinate of the point.
|
|
71
|
+
def initialize(x, y)
|
|
72
|
+
@x = x.to_f
|
|
73
|
+
@y = y.to_f
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Add another point to this one.
|
|
77
|
+
# @param p [Point] The other point.
|
|
78
|
+
# @return [Point] The new point.
|
|
79
|
+
def +(p)
|
|
80
|
+
p = Point.parse(p)
|
|
81
|
+
Point.new(@x + p.x, @y + p.y)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Make all coordinates positive. This is equivalent to reflecting the
|
|
85
|
+
# point about the coordinate axes until it is in the first quadrant.
|
|
86
|
+
# @return (see #+)
|
|
87
|
+
def +@
|
|
88
|
+
Point.new(@x.abs, @y.abs)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Subtract another point to this one.
|
|
92
|
+
# @param (see #+)
|
|
93
|
+
# @return (see #+)
|
|
94
|
+
def -(p)
|
|
95
|
+
p = Point.parse(p)
|
|
96
|
+
Point.new(@x - p.x, @y - p.y)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Take the opposite point with respect to the origin. This is equivalent
|
|
100
|
+
# to performing half a rotation about the origin.
|
|
101
|
+
# @return (see #-)
|
|
102
|
+
def -@
|
|
103
|
+
Point.new(-@x, -@y)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Scale a point or compute the dot product of two points.
|
|
107
|
+
# * If `arg` is Numeric, the point will be scaled by that factor. The
|
|
108
|
+
# return value will then be a new Point.
|
|
109
|
+
# * If `arg` is a Point, the scalar product of the two points will be
|
|
110
|
+
# computed. The return value will then be a Float.
|
|
111
|
+
# @param arg [Numeric,Point] The factor to scale the point.
|
|
112
|
+
# @return [Point,Float] The scaled point or the scalar product.
|
|
113
|
+
def *(arg)
|
|
114
|
+
if Numeric === arg
|
|
115
|
+
Point.new(@x * arg, @y * arg)
|
|
116
|
+
else
|
|
117
|
+
p = Point.parse(arg)
|
|
118
|
+
@x * p.x + @y * p.y
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Scale the point by the inverse of a factor.
|
|
123
|
+
# @param (see #*)
|
|
124
|
+
# @return (see #+)
|
|
125
|
+
def /(s)
|
|
126
|
+
Point.new(@x / s, @y / s)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Project the point onto the given vector. The supplied vector need not be
|
|
130
|
+
# unitary, as it will be normalized automatically.
|
|
131
|
+
# @param (see #+)
|
|
132
|
+
# @return [Point] The new projected point.
|
|
133
|
+
def |(p)
|
|
134
|
+
u = Point.parse(p).normalize
|
|
135
|
+
u * (self * u)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Reflect the point with respect to the given one.
|
|
139
|
+
# @param (see #+)
|
|
140
|
+
# @return [Point] The new reflected point.
|
|
141
|
+
def ^(p)
|
|
142
|
+
self + (Point.parse(p) - self) * 2
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Compute the midpoint between this point and the given one.
|
|
146
|
+
# @param (see #+)
|
|
147
|
+
# @return [Point] The midpoint.
|
|
148
|
+
def &(p)
|
|
149
|
+
(self + Point.parse(p)) / 2
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Return the standard (Cartesian) coordinates of the point. This consists
|
|
153
|
+
# on the X and Y values.
|
|
154
|
+
# @return [Array<Float>] Cartesian coordinates of the point.
|
|
155
|
+
def coords_cartesian
|
|
156
|
+
[@x, @y]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
alias_method :coords, :coords_cartesian
|
|
160
|
+
|
|
161
|
+
# Return the polar coordinates of the point. This consists on the module
|
|
162
|
+
# (euclidean norm) and argument (angle between -PI and PI).
|
|
163
|
+
# @return [Array<Float>] Polar coordinates of the point.
|
|
164
|
+
def coords_polar
|
|
165
|
+
[mod, arg]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
alias_method :polar, :coords_polar
|
|
169
|
+
|
|
170
|
+
# Compute the left-hand (CCW) normal vector.
|
|
171
|
+
# @return [Point] The left-hand normal vector.
|
|
172
|
+
# @see #normal_right
|
|
173
|
+
def normal_left
|
|
174
|
+
Point.new(@y, -@x)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Compute the right-hand (CW) normal vector.
|
|
178
|
+
# @return [Point] The right-hand normal vector.
|
|
179
|
+
# @see #normal_left
|
|
180
|
+
def normal_right
|
|
181
|
+
Point.new(-@y, @x)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
alias_method :normal, :normal_right
|
|
185
|
+
|
|
186
|
+
# Compute the p-norm of the vector. It should be `p>0`.
|
|
187
|
+
# @param p [Float] The parameter of the norm.
|
|
188
|
+
# @return [Float] The p-norm of the vector.
|
|
189
|
+
# @see #norm_1
|
|
190
|
+
# @see #norm
|
|
191
|
+
# @see #norm_inf
|
|
192
|
+
def norm_p(p = 2)
|
|
193
|
+
(@x.abs ** p + @y.abs ** p) ** (1.0 / p)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Shortcut to compute the 1-norm of the vector.
|
|
197
|
+
# @return [Float] The 1-norm of the vector.
|
|
198
|
+
# @see #norm_p
|
|
199
|
+
def norm_1
|
|
200
|
+
(@x.abs + @y.abs).to_f
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Shortcut to compute the euclidean norm of the vector.
|
|
204
|
+
# @return [Float] The euclidean norm of the vector.
|
|
205
|
+
# @see #norm_p
|
|
206
|
+
def norm
|
|
207
|
+
norm_p(2)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
alias_method :norm_2, :norm
|
|
211
|
+
alias_method :mod, :norm
|
|
212
|
+
|
|
213
|
+
# Shortcut to compute the infinity (maximum) norm of the vector.
|
|
214
|
+
# @return [Float] The infinity norm of the vector.
|
|
215
|
+
# @see #norm_p
|
|
216
|
+
def norm_inf
|
|
217
|
+
[@x.abs, @y.abs].max.to_f
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Normalize the vector with respect to the p-norm. It should be `p>0`.
|
|
221
|
+
# @param (see #norm_p)
|
|
222
|
+
# @return [Point] The normalized vector.
|
|
223
|
+
# @see #normalize_1
|
|
224
|
+
# @see #normalize
|
|
225
|
+
# @see #normalize_inf
|
|
226
|
+
# @raise [Exception::GeometryError] If trying to normalize the null vector.
|
|
227
|
+
def normalize_p(p)
|
|
228
|
+
normalize_gen(norm_p(p))
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Shotcut to normalize the vector with respect to the 1-norm.
|
|
232
|
+
# @return (see #normalize_p)
|
|
233
|
+
# @see #normalize_p
|
|
234
|
+
# @raise [Exception::GeometryError] If trying to normalize the null vector.
|
|
235
|
+
def normalize_1
|
|
236
|
+
normalize_p(1)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Shotcut to normalize the vector with respect to the euclidean norm.
|
|
240
|
+
# @return (see #normalize_p)
|
|
241
|
+
# @see #normalize_p
|
|
242
|
+
# @raise [Exception::GeometryError] If trying to normalize the null vector.
|
|
243
|
+
def normalize
|
|
244
|
+
normalize_p(2)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
alias_method :normalize_2, :normalize
|
|
248
|
+
|
|
249
|
+
# Shotcut to normalize the vector with respect to the infinity norm.
|
|
250
|
+
# @return (see #normalize_p)
|
|
251
|
+
# @see #normalize_p
|
|
252
|
+
# @raise [Exception::GeometryError] If trying to normalize the null vector.
|
|
253
|
+
def normalize_inf
|
|
254
|
+
normalize_gen(norm_inf)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Compute the Euclidean distance between two points.
|
|
258
|
+
# @param (see #+)
|
|
259
|
+
# @return
|
|
260
|
+
def distance(p)
|
|
261
|
+
(self - Point.parse(p)).norm
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Project the point onto a line. The line might be supplied by providing
|
|
265
|
+
# either of the following 3 options:
|
|
266
|
+
# * Two different points from the line.
|
|
267
|
+
# * A point and a direction vector (not necessarily normalized).
|
|
268
|
+
# * A point and an angle.
|
|
269
|
+
# At least one point is therefore always required.
|
|
270
|
+
# @param p1 [Point] A point on the line.
|
|
271
|
+
# @param p2 [Point] Another point on the line.
|
|
272
|
+
# @param direction [Point] The direction vector of the line.
|
|
273
|
+
# @param angle [Float] The angle of the line, in radians.
|
|
274
|
+
# @return [Point] The projected point on the line.
|
|
275
|
+
# @raise [Exception::GeometryError] If the line couldn't be determined
|
|
276
|
+
# from the supplied arguments.
|
|
277
|
+
def project(p1: nil, p2: nil, direction: nil, angle: nil)
|
|
278
|
+
raise Exception::GeometryError, "Couldn't determine line,\
|
|
279
|
+
at least one point must be supplied." if !p1 && !p2
|
|
280
|
+
point = Point.parse(p1 || p2)
|
|
281
|
+
direction = Geometry.direction(p1: p1, p2: p2, angle: angle) unless direction
|
|
282
|
+
point - ((point - self) | direction)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Reflect the point with respect to a line. The line might be supplied by
|
|
286
|
+
# providing either of the following 3 options:
|
|
287
|
+
# * Two different points from the line.
|
|
288
|
+
# * A point and a direction vector (not necessarily normalized).
|
|
289
|
+
# * A point and an angle.
|
|
290
|
+
# At least one point is therefore always required.
|
|
291
|
+
# @param t [Float] Proportion of reflection to perform. For example:
|
|
292
|
+
# * `t = 1` will perform the full reflection.
|
|
293
|
+
# * `t = 0` will land on the line.
|
|
294
|
+
# * `t = -1` will not move the point.
|
|
295
|
+
# @param (see #project)
|
|
296
|
+
# @return [Point] The reflected point with respect to the line.
|
|
297
|
+
# @raise (see #project)
|
|
298
|
+
def reflect(t = 1, p1: nil, p2: nil, direction: nil, angle: nil)
|
|
299
|
+
proj = self.project(p1: p1, p2: p2, direction: direction, angle: angle)
|
|
300
|
+
self + (proj - self) * (t + 1)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Return the angle (argument) of the point. It is expressed in radian,
|
|
304
|
+
# between -PI and PI.
|
|
305
|
+
# @return [Float] Angle of the point.
|
|
306
|
+
def arg
|
|
307
|
+
Math.atan2(@y, @x)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Whether the point is null, i.e., close enough to the origin.
|
|
311
|
+
# @return [Boolean] Whether the point is (close enough to) the origin.
|
|
312
|
+
def zero?
|
|
313
|
+
norm < PRECISION
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Find the angle between this point and the given one. The angle will be
|
|
317
|
+
# in the interval [0, PI].
|
|
318
|
+
# @param (see #+)
|
|
319
|
+
# @return [Float] Angle between the points.
|
|
320
|
+
def angle(p)
|
|
321
|
+
p = Point.parse(p)
|
|
322
|
+
Math.acos((self * p) / (norm * p.norm))
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Find whether the given point is perpendicular to this one.
|
|
326
|
+
# @param (see #+)
|
|
327
|
+
# @return [Boolean] Whether the points are orthogonal.
|
|
328
|
+
def orthogonal?(p)
|
|
329
|
+
(self * Point.parse(p)).abs < PRECISION
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
alias_method :perpendicular?, :orthogonal?
|
|
333
|
+
|
|
334
|
+
# Find whether the given point / vector is parallel (proportional) to
|
|
335
|
+
# this one.
|
|
336
|
+
# @param (see #+)
|
|
337
|
+
# @return [Boolean] Whether the points are parallel.
|
|
338
|
+
def parallel?(p)
|
|
339
|
+
angle(p).abs < PRECISION
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Find whether the points are positively aligned. This means that their
|
|
343
|
+
# scalar product is positive, and implies that they form an acute angle,
|
|
344
|
+
# i.e., they go roughly in the same direction.
|
|
345
|
+
# @param (see #+)
|
|
346
|
+
# @return [Boolean] Whether the points are positively aligned.
|
|
347
|
+
def positive?(p)
|
|
348
|
+
self * p > 0
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Find whether the points are negatively aligned. This means that their
|
|
352
|
+
# scalar product is negative, and implies that they form an obtuse angle,
|
|
353
|
+
# i.e., they go roughly in the opposite direction.
|
|
354
|
+
# @param (see #+)
|
|
355
|
+
# @return [Boolean] Whether the points are negatively aligned.
|
|
356
|
+
def negative?(p)
|
|
357
|
+
self * p < 0
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Rotate the point by a certain angle about a given center.
|
|
361
|
+
# @param angle [Float] The angle to rotate the point, in radians.
|
|
362
|
+
# @param center [Point] The point to rotate about.
|
|
363
|
+
# @return (see #+)
|
|
364
|
+
def rotate(angle, center = ORIGIN)
|
|
365
|
+
center = Point.parse(center)
|
|
366
|
+
x_old = @x - center.x
|
|
367
|
+
y_old = @y - center.y
|
|
368
|
+
sin = Math.sin(angle)
|
|
369
|
+
cos = Math.cos(angle)
|
|
370
|
+
x = x_old * cos - y_old * sin
|
|
371
|
+
y = x_old * sin + y_old * cos
|
|
372
|
+
Point.new(x + center.x, y + center.y)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Shortcut to rotate the point 90 degrees counterclockwise about a given
|
|
376
|
+
# center.
|
|
377
|
+
# @param center [Point] The point to rotate about.
|
|
378
|
+
# @return (see #+)
|
|
379
|
+
def rotate_left(center = ORIGIN)
|
|
380
|
+
rotate(-Math::PI / 2, center)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Shortcut to rotate the point 90 degrees clockwise about a given center.
|
|
384
|
+
# @param (see #rotate_left)
|
|
385
|
+
# @return (see #+)
|
|
386
|
+
def rotate_right(center = ORIGIN)
|
|
387
|
+
rotate(Math::PI / 2, center)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Shortcut to rotate the point 180 degrees about a given center.
|
|
391
|
+
# @param (see #rotate_left)
|
|
392
|
+
# @return (see #+)
|
|
393
|
+
def rotate_180(center = ORIGIN)
|
|
394
|
+
rotate(Math::PI, center)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
alias_method :translate, :+
|
|
398
|
+
alias_method :scale, :*
|
|
399
|
+
|
|
400
|
+
# Convert to integer point by rounding the coordinates.
|
|
401
|
+
# @return (see #+)
|
|
402
|
+
# @see #to_i
|
|
403
|
+
# @see #floor
|
|
404
|
+
# @see #ceil
|
|
405
|
+
def round
|
|
406
|
+
Point.new(@x.round, @y.round)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Convert to integer point by taking the integer part of the coordinates.
|
|
410
|
+
# @return (see #+)
|
|
411
|
+
# @see #floor
|
|
412
|
+
# @see #ceil
|
|
413
|
+
# @see #round
|
|
414
|
+
def to_i
|
|
415
|
+
Point.new(@x.to_i, @y.to_i)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
alias_method :truncate, :to_i
|
|
419
|
+
|
|
420
|
+
# Convert to integer point by taking the floor part of the coordinates.
|
|
421
|
+
# @return (see #+)
|
|
422
|
+
# @see #to_i
|
|
423
|
+
# @see #ceil
|
|
424
|
+
# @see #round
|
|
425
|
+
def floor
|
|
426
|
+
Point.new(@x.floor, @y.floor)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Convert to integer point by taking the ceiling part of the coordinates.
|
|
430
|
+
# @return (see #+)
|
|
431
|
+
# @see #to_i
|
|
432
|
+
# @see #floor
|
|
433
|
+
# @see #round
|
|
434
|
+
def ceil
|
|
435
|
+
Point.new(@x.ceil, @y.ceil)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Format the point's coordinates in the usual form.
|
|
439
|
+
# @return [String] The formatted point.
|
|
440
|
+
def to_s
|
|
441
|
+
"(#{@x}, #{@y})"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
# Normalize the vector with respect to an arbitrary norm.
|
|
447
|
+
def normalize_gen(norm)
|
|
448
|
+
raise Exception::GeometryError, "Cannot normalize null vector." if zero?
|
|
449
|
+
Point.new(@x / norm, @y / norm)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
end # Class Point
|
|
453
|
+
|
|
454
|
+
# Precision of the floating point math. Anything below this threshold will
|
|
455
|
+
# be considered 0.
|
|
456
|
+
# @return [Float] Floating point math precision.
|
|
457
|
+
PRECISION = 1E-7
|
|
458
|
+
|
|
459
|
+
# The point representing the origin of coordinates.
|
|
460
|
+
# @return [Point] Origin of coordinates.
|
|
461
|
+
ORIGIN = Point.new(0, 0)
|
|
462
|
+
|
|
463
|
+
# The point representing the first vector of the canonical base.
|
|
464
|
+
# @return [Point] First canonical vector.
|
|
465
|
+
E1 = Point.new(1, 0)
|
|
466
|
+
|
|
467
|
+
# The point representing the second vector of the canonical base.
|
|
468
|
+
# @return [Point] Second canonical vector.
|
|
469
|
+
E2 = Point.new(0, 1)
|
|
470
|
+
|
|
7
471
|
# Finds the endpoint of a line given the startpoint and something else.
|
|
8
472
|
# Namely, either of the following:
|
|
9
473
|
# * The displacement vector (`vector`).
|
|
@@ -19,29 +483,44 @@ module Gifenc
|
|
|
19
483
|
# @param length [Float] The length of the line. Must be provided if either
|
|
20
484
|
# the `direction` or the `angle` method is being used.
|
|
21
485
|
# @return [Array<Integer>] The [X, Y] coordinates of the line's endpoint.
|
|
22
|
-
# @raise [Exception::
|
|
486
|
+
# @raise [Exception::GeometryError] If the supplied parameters don't suffice
|
|
23
487
|
# to determine a line (e.g. provided the `angle` but not the `length`).
|
|
24
488
|
def self.endpoint(
|
|
25
489
|
point: nil, vector: nil, direction: nil, angle: nil, length: nil
|
|
26
490
|
)
|
|
27
|
-
raise Exception::
|
|
491
|
+
raise Exception::GeometryError, "The line start must be specified." if !point
|
|
492
|
+
point = Point.parse(point)
|
|
28
493
|
if vector
|
|
29
|
-
|
|
30
|
-
|
|
494
|
+
vector = Point.parse(vector)
|
|
495
|
+
x1 = point.x + vector.x
|
|
496
|
+
y1 = point.y + vector.y
|
|
31
497
|
else
|
|
32
|
-
raise Exception::
|
|
498
|
+
raise Exception::GeometryError, "Either the endpoint, the vector or the length must be provided." if !length
|
|
33
499
|
if direction
|
|
34
|
-
|
|
35
|
-
direction[0] /= mod
|
|
36
|
-
direction[1] /= mod
|
|
500
|
+
direction = Point.parse(direction).normalize
|
|
37
501
|
else
|
|
38
|
-
raise Exception::
|
|
39
|
-
direction =
|
|
502
|
+
raise Exception::GeometryError, "The angle must be specified if no direction is provided." if !angle
|
|
503
|
+
direction = Point.new(Math.cos(angle), Math.sin(angle))
|
|
40
504
|
end
|
|
41
|
-
x1 = (point
|
|
42
|
-
y1 = (point
|
|
505
|
+
x1 = (point.x + length * direction.x).to_i
|
|
506
|
+
y1 = (point.y + length * direction.y).to_i
|
|
43
507
|
end
|
|
44
|
-
|
|
508
|
+
Point.new(x1, y1)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Find the unit direction vector of a line given either the endpoints or
|
|
512
|
+
# the angle.
|
|
513
|
+
# @param p1 [Point] One point of the line.
|
|
514
|
+
# @param p2 [Point] Another point of the line.
|
|
515
|
+
# @param angle [Float] The angle in radians.
|
|
516
|
+
# @return [Point] The unit direction vector.
|
|
517
|
+
# @raise [Exception::GeometryError] If not enough information is supplied
|
|
518
|
+
# (either the endpoints or the angle is required).
|
|
519
|
+
def self.direction(p1: nil, p2: nil, angle: nil)
|
|
520
|
+
return Point.parse([1, angle], :polar) if angle
|
|
521
|
+
raise Exception::GeometryError, "Couldn't parse direction, endpoints or|
|
|
522
|
+
angle must be supplied." if !p1 || !p2
|
|
523
|
+
(Point.parse(p1) - Point.parse(p2)).normalize
|
|
45
524
|
end
|
|
46
525
|
|
|
47
526
|
# Finds the bounding box of a set of points, i.e., the minimal rectangle
|
|
@@ -55,10 +534,11 @@ module Gifenc
|
|
|
55
534
|
# `[X, Y]` are the coordinates of the upper left corner of the rectangle,
|
|
56
535
|
# and `[W, H]` are its width and height, respectively.
|
|
57
536
|
def self.bbox(points, pad = 0)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
537
|
+
points = points.map{ |p| Point.parse(p) }
|
|
538
|
+
x0 = points.min_by(&:x).x.round - pad
|
|
539
|
+
y0 = points.min_by(&:y).y.round - pad
|
|
540
|
+
x1 = points.max_by(&:x).x.round + pad
|
|
541
|
+
y1 = points.max_by(&:y).y.round + pad
|
|
62
542
|
[x0, y0, x1 - x0 + 1, y1 - y0 + 1]
|
|
63
543
|
end
|
|
64
544
|
|
|
@@ -71,7 +551,8 @@ module Gifenc
|
|
|
71
551
|
# the supplied points.
|
|
72
552
|
# @return [Array<Array<Integer>>] The list of translated points.
|
|
73
553
|
def self.translate(points, vector)
|
|
74
|
-
|
|
554
|
+
vector = Point.parse(vector)
|
|
555
|
+
points.map{ |p| Point.parse(p) + vector }
|
|
75
556
|
end
|
|
76
557
|
|
|
77
558
|
# Computes the coordinates of a list of points relative to a provided bbox.
|
|
@@ -92,6 +573,43 @@ module Gifenc
|
|
|
92
573
|
translate(points, [-bbox[0], -bbox[1]])
|
|
93
574
|
end
|
|
94
575
|
|
|
576
|
+
# Compute the convex hull of a set of points. The convex hull is the smallest
|
|
577
|
+
# convex set containing the supplied points. This method will return the
|
|
578
|
+
# points located in the boundary of said hull in CCW order. The interior of
|
|
579
|
+
# the polygon they delimit is thus the convex hull.
|
|
580
|
+
#
|
|
581
|
+
# The *reduced* version will only include the extreme points of the boundary,
|
|
582
|
+
# i.e. the vertices, as opposed to all of them. The algorithm employed in
|
|
583
|
+
# both cases is the most basic one, known as the *Jarvis march*.
|
|
584
|
+
# @param points [Array<Point>] The list of points.
|
|
585
|
+
# @param reduced [Boolean] Whether to only return the vertices of the hull.
|
|
586
|
+
# @return [Array<Point>] The points composing the boundary of the convex hull.
|
|
587
|
+
def self.convex_hull(points, reduced = false)
|
|
588
|
+
points = points.uniq.map{ |p| Point.parse(p) }
|
|
589
|
+
return points if points.size < 3
|
|
590
|
+
hull_1 = points.min_by{ |p| [p.x, p.y] }
|
|
591
|
+
hull_2 = nil
|
|
592
|
+
hull_old = nil
|
|
593
|
+
hull = []
|
|
594
|
+
until hull_1 == hull.first
|
|
595
|
+
hull << hull_1
|
|
596
|
+
hull_2 = (points[0 ... 3] - [hull_1, hull_old]).first
|
|
597
|
+
points.each{ |p|
|
|
598
|
+
next if p == hull_1 || p == hull_2 || p == hull_old
|
|
599
|
+
index = cw(hull_1, p, hull_2)
|
|
600
|
+
next if index > PRECISION
|
|
601
|
+
if index.abs < PRECISION
|
|
602
|
+
d_new = hull_1.distance(p)
|
|
603
|
+
d_old = hull_1.distance(hull_2)
|
|
604
|
+
end
|
|
605
|
+
hull_2 = p if index < -PRECISION || (reduced ? d_new > d_old : d_new < d_old)
|
|
606
|
+
}
|
|
607
|
+
hull_old = hull_1
|
|
608
|
+
hull_1 = hull_2
|
|
609
|
+
end
|
|
610
|
+
hull
|
|
611
|
+
end
|
|
612
|
+
|
|
95
613
|
# Checks if a list of points is entirely contained in the specified bounding
|
|
96
614
|
# box.
|
|
97
615
|
# @param (see #transform)
|
|
@@ -101,18 +619,76 @@ module Gifenc
|
|
|
101
619
|
# @raise [Exception::CanvasError] If the points are not contained in the
|
|
102
620
|
# bounding box and `silent` has not been set.
|
|
103
621
|
def self.bound_check(points, bbox, silent = false)
|
|
622
|
+
bbox = [0, 0, bbox.width, bbox.height] if bbox.is_a?(Image)
|
|
623
|
+
points.map!{ |p| Point.parse(p) }
|
|
104
624
|
outer_points = points.select{ |p|
|
|
105
|
-
!p
|
|
106
|
-
!p
|
|
625
|
+
!p.x.between?(bbox[0], bbox[0] + bbox[2] - 1) ||
|
|
626
|
+
!p.y.between?(bbox[1], bbox[1] + bbox[3] - 1)
|
|
107
627
|
}
|
|
108
628
|
if outer_points.size > 0
|
|
109
629
|
return false if silent
|
|
110
|
-
points_str = outer_points.take(3).map{ |p| "(#{p
|
|
630
|
+
points_str = outer_points.take(3).map{ |p| "(#{p.x}, #{p.y})" }
|
|
111
631
|
.join(', ') + '...'
|
|
112
632
|
raise Exception::CanvasError, "Out of bounds pixels found: #{points_str}"
|
|
113
633
|
end
|
|
114
634
|
true
|
|
115
635
|
end
|
|
116
636
|
|
|
117
|
-
|
|
118
|
-
|
|
637
|
+
# Compute a linear combination of points given the weights. The two supplied
|
|
638
|
+
# arrays should have the same length.
|
|
639
|
+
# @param points [Array<Point>] The points to combine.
|
|
640
|
+
# @param weights [Array<Float>] The weights to utilize for each point.
|
|
641
|
+
# @return [Point] The resulting linear combination.
|
|
642
|
+
# @raise [Exception::GeometryError] If the arrays' sizes differ.
|
|
643
|
+
def self.comb_linear(points, weights)
|
|
644
|
+
if points.size != weights.size
|
|
645
|
+
raise Exception::GeometryError, "Point and weight counts differ."
|
|
646
|
+
end
|
|
647
|
+
return ORIGIN if points.size == 0
|
|
648
|
+
|
|
649
|
+
points.map!{ |p| Point.parse(p) }
|
|
650
|
+
res = points[0] * weights[0]
|
|
651
|
+
points[1..-1].each_with_index{ |p, i|
|
|
652
|
+
res += p * weights[i + 1]
|
|
653
|
+
}
|
|
654
|
+
res
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Compute a convex combination of points given the weights. This is simply
|
|
658
|
+
# a linear combination, but the weights are normalized so they sum to 1.
|
|
659
|
+
# If they're positive, the resulting point will always be contained within
|
|
660
|
+
# the convex hull of the supplied points, hence the name. The two provided
|
|
661
|
+
# arrays should have the same length.
|
|
662
|
+
# @param (see #comb_linear)
|
|
663
|
+
# @return [Point] The resulting convex combination.
|
|
664
|
+
# @raise [Exception::GeometryError] If the arrays's sizes differ, or if the
|
|
665
|
+
# weights sum to 0.
|
|
666
|
+
def self.comb_convex(points, weights)
|
|
667
|
+
return ORIGIN if points.size == 0
|
|
668
|
+
weight = weights.sum
|
|
669
|
+
raise Exception::GeometryError, "Cannot find convex combination, weights sum to 0." if weight.abs < PRECISION
|
|
670
|
+
comb_linear(points, weights.map{ |w| w.to_f / weight })
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Find the center of mass (barycenter) of a list of points. This will always
|
|
674
|
+
# be contained within the convex hull of the supplied points.
|
|
675
|
+
# @param points [Array<Point>] The list of points.
|
|
676
|
+
# @return [Point]
|
|
677
|
+
def self.center(points)
|
|
678
|
+
raise Exception::GeometryError, "Cannot find center of empty list of points." if points.size == 0
|
|
679
|
+
points.map!{ |p| Point.parse(p) }
|
|
680
|
+
points.sum(ORIGIN) / points.size
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
private
|
|
684
|
+
|
|
685
|
+
# Index that indicates the clockwise order of 3 points:
|
|
686
|
+
# < 0 --> CCW
|
|
687
|
+
# = 0 --> Aligned
|
|
688
|
+
# > 0 --> CW
|
|
689
|
+
def self.cw(p, q, r)
|
|
690
|
+
(r.y - p.y) * (q.x - p.x) - (q.y - p.y) * (r.x - p.x)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
end # Module Geometry
|
|
694
|
+
end # Module Gifenc
|