lib/goog/math/rect.js

1// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview A utility class for representing rectangles.
17 */
18
19goog.provide('goog.math.Rect');
20
21goog.require('goog.math.Box');
22goog.require('goog.math.Coordinate');
23goog.require('goog.math.Size');
24
25
26
27/**
28 * Class for representing rectangular regions.
29 * @param {number} x Left.
30 * @param {number} y Top.
31 * @param {number} w Width.
32 * @param {number} h Height.
33 * @struct
34 * @constructor
35 */
36goog.math.Rect = function(x, y, w, h) {
37 /** @type {number} */
38 this.left = x;
39
40 /** @type {number} */
41 this.top = y;
42
43 /** @type {number} */
44 this.width = w;
45
46 /** @type {number} */
47 this.height = h;
48};
49
50
51/**
52 * @return {!goog.math.Rect} A new copy of this Rectangle.
53 */
54goog.math.Rect.prototype.clone = function() {
55 return new goog.math.Rect(this.left, this.top, this.width, this.height);
56};
57
58
59/**
60 * Returns a new Box object with the same position and dimensions as this
61 * rectangle.
62 * @return {!goog.math.Box} A new Box representation of this Rectangle.
63 */
64goog.math.Rect.prototype.toBox = function() {
65 var right = this.left + this.width;
66 var bottom = this.top + this.height;
67 return new goog.math.Box(this.top,
68 right,
69 bottom,
70 this.left);
71};
72
73
74/**
75 * Creates a new Rect object with the position and size given.
76 * @param {!goog.math.Coordinate} position The top-left coordinate of the Rect
77 * @param {!goog.math.Size} size The size of the Rect
78 * @return {!goog.math.Rect} A new Rect initialized with the given position and
79 * size.
80 */
81goog.math.Rect.createFromPositionAndSize = function(position, size) {
82 return new goog.math.Rect(position.x, position.y, size.width, size.height);
83};
84
85
86/**
87 * Creates a new Rect object with the same position and dimensions as a given
88 * Box. Note that this is only the inverse of toBox if left/top are defined.
89 * @param {goog.math.Box} box A box.
90 * @return {!goog.math.Rect} A new Rect initialized with the box's position
91 * and size.
92 */
93goog.math.Rect.createFromBox = function(box) {
94 return new goog.math.Rect(box.left, box.top,
95 box.right - box.left, box.bottom - box.top);
96};
97
98
99if (goog.DEBUG) {
100 /**
101 * Returns a nice string representing size and dimensions of rectangle.
102 * @return {string} In the form (50, 73 - 75w x 25h).
103 * @override
104 */
105 goog.math.Rect.prototype.toString = function() {
106 return '(' + this.left + ', ' + this.top + ' - ' + this.width + 'w x ' +
107 this.height + 'h)';
108 };
109}
110
111
112/**
113 * Compares rectangles for equality.
114 * @param {goog.math.Rect} a A Rectangle.
115 * @param {goog.math.Rect} b A Rectangle.
116 * @return {boolean} True iff the rectangles have the same left, top, width,
117 * and height, or if both are null.
118 */
119goog.math.Rect.equals = function(a, b) {
120 if (a == b) {
121 return true;
122 }
123 if (!a || !b) {
124 return false;
125 }
126 return a.left == b.left && a.width == b.width &&
127 a.top == b.top && a.height == b.height;
128};
129
130
131/**
132 * Computes the intersection of this rectangle and the rectangle parameter. If
133 * there is no intersection, returns false and leaves this rectangle as is.
134 * @param {goog.math.Rect} rect A Rectangle.
135 * @return {boolean} True iff this rectangle intersects with the parameter.
136 */
137goog.math.Rect.prototype.intersection = function(rect) {
138 var x0 = Math.max(this.left, rect.left);
139 var x1 = Math.min(this.left + this.width, rect.left + rect.width);
140
141 if (x0 <= x1) {
142 var y0 = Math.max(this.top, rect.top);
143 var y1 = Math.min(this.top + this.height, rect.top + rect.height);
144
145 if (y0 <= y1) {
146 this.left = x0;
147 this.top = y0;
148 this.width = x1 - x0;
149 this.height = y1 - y0;
150
151 return true;
152 }
153 }
154 return false;
155};
156
157
158/**
159 * Returns the intersection of two rectangles. Two rectangles intersect if they
160 * touch at all, for example, two zero width and height rectangles would
161 * intersect if they had the same top and left.
162 * @param {goog.math.Rect} a A Rectangle.
163 * @param {goog.math.Rect} b A Rectangle.
164 * @return {goog.math.Rect} A new intersection rect (even if width and height
165 * are 0), or null if there is no intersection.
166 */
167goog.math.Rect.intersection = function(a, b) {
168 // There is no nice way to do intersection via a clone, because any such
169 // clone might be unnecessary if this function returns null. So, we duplicate
170 // code from above.
171
172 var x0 = Math.max(a.left, b.left);
173 var x1 = Math.min(a.left + a.width, b.left + b.width);
174
175 if (x0 <= x1) {
176 var y0 = Math.max(a.top, b.top);
177 var y1 = Math.min(a.top + a.height, b.top + b.height);
178
179 if (y0 <= y1) {
180 return new goog.math.Rect(x0, y0, x1 - x0, y1 - y0);
181 }
182 }
183 return null;
184};
185
186
187/**
188 * Returns whether two rectangles intersect. Two rectangles intersect if they
189 * touch at all, for example, two zero width and height rectangles would
190 * intersect if they had the same top and left.
191 * @param {goog.math.Rect} a A Rectangle.
192 * @param {goog.math.Rect} b A Rectangle.
193 * @return {boolean} Whether a and b intersect.
194 */
195goog.math.Rect.intersects = function(a, b) {
196 return (a.left <= b.left + b.width && b.left <= a.left + a.width &&
197 a.top <= b.top + b.height && b.top <= a.top + a.height);
198};
199
200
201/**
202 * Returns whether a rectangle intersects this rectangle.
203 * @param {goog.math.Rect} rect A rectangle.
204 * @return {boolean} Whether rect intersects this rectangle.
205 */
206goog.math.Rect.prototype.intersects = function(rect) {
207 return goog.math.Rect.intersects(this, rect);
208};
209
210
211/**
212 * Computes the difference regions between two rectangles. The return value is
213 * an array of 0 to 4 rectangles defining the remaining regions of the first
214 * rectangle after the second has been subtracted.
215 * @param {goog.math.Rect} a A Rectangle.
216 * @param {goog.math.Rect} b A Rectangle.
217 * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
218 * together define the difference area of rectangle a minus rectangle b.
219 */
220goog.math.Rect.difference = function(a, b) {
221 var intersection = goog.math.Rect.intersection(a, b);
222 if (!intersection || !intersection.height || !intersection.width) {
223 return [a.clone()];
224 }
225
226 var result = [];
227
228 var top = a.top;
229 var height = a.height;
230
231 var ar = a.left + a.width;
232 var ab = a.top + a.height;
233
234 var br = b.left + b.width;
235 var bb = b.top + b.height;
236
237 // Subtract off any area on top where A extends past B
238 if (b.top > a.top) {
239 result.push(new goog.math.Rect(a.left, a.top, a.width, b.top - a.top));
240 top = b.top;
241 // If we're moving the top down, we also need to subtract the height diff.
242 height -= b.top - a.top;
243 }
244 // Subtract off any area on bottom where A extends past B
245 if (bb < ab) {
246 result.push(new goog.math.Rect(a.left, bb, a.width, ab - bb));
247 height = bb - top;
248 }
249 // Subtract any area on left where A extends past B
250 if (b.left > a.left) {
251 result.push(new goog.math.Rect(a.left, top, b.left - a.left, height));
252 }
253 // Subtract any area on right where A extends past B
254 if (br < ar) {
255 result.push(new goog.math.Rect(br, top, ar - br, height));
256 }
257
258 return result;
259};
260
261
262/**
263 * Computes the difference regions between this rectangle and {@code rect}. The
264 * return value is an array of 0 to 4 rectangles defining the remaining regions
265 * of this rectangle after the other has been subtracted.
266 * @param {goog.math.Rect} rect A Rectangle.
267 * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
268 * together define the difference area of rectangle a minus rectangle b.
269 */
270goog.math.Rect.prototype.difference = function(rect) {
271 return goog.math.Rect.difference(this, rect);
272};
273
274
275/**
276 * Expand this rectangle to also include the area of the given rectangle.
277 * @param {goog.math.Rect} rect The other rectangle.
278 */
279goog.math.Rect.prototype.boundingRect = function(rect) {
280 // We compute right and bottom before we change left and top below.
281 var right = Math.max(this.left + this.width, rect.left + rect.width);
282 var bottom = Math.max(this.top + this.height, rect.top + rect.height);
283
284 this.left = Math.min(this.left, rect.left);
285 this.top = Math.min(this.top, rect.top);
286
287 this.width = right - this.left;
288 this.height = bottom - this.top;
289};
290
291
292/**
293 * Returns a new rectangle which completely contains both input rectangles.
294 * @param {goog.math.Rect} a A rectangle.
295 * @param {goog.math.Rect} b A rectangle.
296 * @return {goog.math.Rect} A new bounding rect, or null if either rect is
297 * null.
298 */
299goog.math.Rect.boundingRect = function(a, b) {
300 if (!a || !b) {
301 return null;
302 }
303
304 var clone = a.clone();
305 clone.boundingRect(b);
306
307 return clone;
308};
309
310
311/**
312 * Tests whether this rectangle entirely contains another rectangle or
313 * coordinate.
314 *
315 * @param {goog.math.Rect|goog.math.Coordinate} another The rectangle or
316 * coordinate to test for containment.
317 * @return {boolean} Whether this rectangle contains given rectangle or
318 * coordinate.
319 */
320goog.math.Rect.prototype.contains = function(another) {
321 if (another instanceof goog.math.Rect) {
322 return this.left <= another.left &&
323 this.left + this.width >= another.left + another.width &&
324 this.top <= another.top &&
325 this.top + this.height >= another.top + another.height;
326 } else { // (another instanceof goog.math.Coordinate)
327 return another.x >= this.left &&
328 another.x <= this.left + this.width &&
329 another.y >= this.top &&
330 another.y <= this.top + this.height;
331 }
332};
333
334
335/**
336 * @param {!goog.math.Coordinate} point A coordinate.
337 * @return {number} The squared distance between the point and the closest
338 * point inside the rectangle. Returns 0 if the point is inside the
339 * rectangle.
340 */
341goog.math.Rect.prototype.squaredDistance = function(point) {
342 var dx = point.x < this.left ?
343 this.left - point.x : Math.max(point.x - (this.left + this.width), 0);
344 var dy = point.y < this.top ?
345 this.top - point.y : Math.max(point.y - (this.top + this.height), 0);
346 return dx * dx + dy * dy;
347};
348
349
350/**
351 * @param {!goog.math.Coordinate} point A coordinate.
352 * @return {number} The distance between the point and the closest point
353 * inside the rectangle. Returns 0 if the point is inside the rectangle.
354 */
355goog.math.Rect.prototype.distance = function(point) {
356 return Math.sqrt(this.squaredDistance(point));
357};
358
359
360/**
361 * @return {!goog.math.Size} The size of this rectangle.
362 */
363goog.math.Rect.prototype.getSize = function() {
364 return new goog.math.Size(this.width, this.height);
365};
366
367
368/**
369 * @return {!goog.math.Coordinate} A new coordinate for the top-left corner of
370 * the rectangle.
371 */
372goog.math.Rect.prototype.getTopLeft = function() {
373 return new goog.math.Coordinate(this.left, this.top);
374};
375
376
377/**
378 * @return {!goog.math.Coordinate} A new coordinate for the center of the
379 * rectangle.
380 */
381goog.math.Rect.prototype.getCenter = function() {
382 return new goog.math.Coordinate(
383 this.left + this.width / 2, this.top + this.height / 2);
384};
385
386
387/**
388 * @return {!goog.math.Coordinate} A new coordinate for the bottom-right corner
389 * of the rectangle.
390 */
391goog.math.Rect.prototype.getBottomRight = function() {
392 return new goog.math.Coordinate(
393 this.left + this.width, this.top + this.height);
394};
395
396
397/**
398 * Rounds the fields to the next larger integer values.
399 * @return {!goog.math.Rect} This rectangle with ceil'd fields.
400 */
401goog.math.Rect.prototype.ceil = function() {
402 this.left = Math.ceil(this.left);
403 this.top = Math.ceil(this.top);
404 this.width = Math.ceil(this.width);
405 this.height = Math.ceil(this.height);
406 return this;
407};
408
409
410/**
411 * Rounds the fields to the next smaller integer values.
412 * @return {!goog.math.Rect} This rectangle with floored fields.
413 */
414goog.math.Rect.prototype.floor = function() {
415 this.left = Math.floor(this.left);
416 this.top = Math.floor(this.top);
417 this.width = Math.floor(this.width);
418 this.height = Math.floor(this.height);
419 return this;
420};
421
422
423/**
424 * Rounds the fields to nearest integer values.
425 * @return {!goog.math.Rect} This rectangle with rounded fields.
426 */
427goog.math.Rect.prototype.round = function() {
428 this.left = Math.round(this.left);
429 this.top = Math.round(this.top);
430 this.width = Math.round(this.width);
431 this.height = Math.round(this.height);
432 return this;
433};
434
435
436/**
437 * Translates this rectangle by the given offsets. If a
438 * {@code goog.math.Coordinate} is given, then the left and top values are
439 * translated by the coordinate's x and y values. Otherwise, top and left are
440 * translated by {@code tx} and {@code opt_ty} respectively.
441 * @param {number|goog.math.Coordinate} tx The value to translate left by or the
442 * the coordinate to translate this rect by.
443 * @param {number=} opt_ty The value to translate top by.
444 * @return {!goog.math.Rect} This rectangle after translating.
445 */
446goog.math.Rect.prototype.translate = function(tx, opt_ty) {
447 if (tx instanceof goog.math.Coordinate) {
448 this.left += tx.x;
449 this.top += tx.y;
450 } else {
451 this.left += tx;
452 if (goog.isNumber(opt_ty)) {
453 this.top += opt_ty;
454 }
455 }
456 return this;
457};
458
459
460/**
461 * Scales this rectangle by the given scale factors. The left and width values
462 * are scaled by {@code sx} and the top and height values are scaled by
463 * {@code opt_sy}. If {@code opt_sy} is not given, then all fields are scaled
464 * by {@code sx}.
465 * @param {number} sx The scale factor to use for the x dimension.
466 * @param {number=} opt_sy The scale factor to use for the y dimension.
467 * @return {!goog.math.Rect} This rectangle after scaling.
468 */
469goog.math.Rect.prototype.scale = function(sx, opt_sy) {
470 var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
471 this.left *= sx;
472 this.width *= sx;
473 this.top *= sy;
474 this.height *= sy;
475 return this;
476};