var _ = require("lodash");
var validate = require("./validate");
/**
* Utility module for dealing with rectangles.
*
* @module decks/utils/rect
*/
module.exports = {
/**
* Normalizes an object that has top, bottom, left, and right properties, to a plain
* object with top, bottom, left, right, width, and height properties.
*
* @param {*} r object that has top, bottom, left, right properties. If r is an Element, r will be the
* result of r.getBoundingClientRect()
* @return {undefined}
*/
normalize: function normalize(r) {
validate(r, "rect#normalize: r (rectangle object)", { isRequired: true });
if (r.isNormalized) {
return r;
}
if (_.isElement(r)) {
r = this.getElementRect(r);
}
// TODO: should this check for correctness of all values? E.g. top < bottom, left < right, width === right - left, etc.
var top = _.isFinite(r.top) ? r.top : 0;
var bottom = _.isFinite(r.bottom) ? r.bottom : 0;
var left = _.isFinite(r.left) ? r.left : 0;
var right = _.isFinite(r.right) ? r.right : 0;
var width = _.isFinite(r.width) ? r.width : (right - left);
var height = _.isFinite(r.height) ? r.height : (bottom - top);
return {
isNormalized: true,
top: top,
bottom: bottom,
left: left,
right: right,
width: width,
height: height
};
},
/**
* Gets a bounding client rect for an element, with respect to the document coordinate
* system (not the window coordinate system).
*
* @param {HTMLElement} element - the element for which to find the rect
* @return {Object} - the calculated rect, with respect to the document coordinate system
*/
getElementRect: function getElementRect(element) {
validate(element, "rect#getElementRect: element", { isElement: true });
return element.getBoundingClientRect();
},
/**
* Indicates if two rects are equal in all dimensions/values.
*
* @param r1
* @param r2
* @return {undefined}
*/
isEqual: function isEqual(r1, r2) {
if (!r1 && !r2) {
return true;
}
if (!r1 || !r2) {
return false;
}
r1 = this.normalize(r1);
r2 = this.normalize(r2);
return _.isEqual(r1, r2);
},
/**
* Indicates if the rect is empty (null, undefined, or or has all values equal to 0)
*
* @param r
* @return {undefined}
*/
isEmpty: function isEmpty(r) {
if (_.isNull(r) || _.isUndefined(r)) {
return true;
}
return _.all(["left", "right", "top", "bottom", "width", "height"], function(key) {
return r[key] === 0;
});
},
/**
* Indicates whether rectangle r1 intersects rectangle r2
*
* @param {*} r1 first rectangle
* @param {*} r2 second rectangle
* @return {boolean} true if r1 intersects r2, otherwise false
*/
intersects: function intersects(r1, r2) {
r1 = this.normalize(r1);
r2 = this.normalize(r2);
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
},
/**
* Returns a new rectangle which is the union of rectangles r1 and r2
*
* @param {*} r1 rectangle 1
* @param {*} r2 rectangle 2
* @return {*} New rectangle that is the union of r1 and r2
*/
union: function union(r1, r2) {
r1 = this.normalize(r1);
r2 = this.normalize(r2);
return this.normalize({
top: Math.min(r1.top, r2.top),
bottom: Math.max(r1.bottom, r2.bottom),
left: Math.min(r1.left, r2.left),
right: Math.max(r1.right, r2.right)
});
},
/**
* Returns a new rectangle which is the union of all the given rectangles
*
* @param {Array} rects
* @return {*} New rectangle
*/
unionAll: function unionAll(rects) {
validate(rects, "rect#unionAll: rects", { isArray: true });
// Initial accumulator allows the first rectangle to win the union
var acc = {
top: Infinity,
bottom: -Infinity,
left: Infinity,
right: -Infinity
};
return _.reduce(rects, function(acc, rect) {
return this.union(acc, rect);
}, acc, this);
},
/**
* Resizes a rect by adding the specified width and height values, and adjusting
* the right and bottom dimensions, based on the current top/left and new width/height.
*
* Use negative width and height to shrink the rect.
*
* This does not change top and left.
*
* @param {!Object} r - rectangle-like object
* @param {?number} [width=0] - delta value for width
* @param {?number} [height=0] - delta value for height
* @return {Object} resulting normalized rectangle object
*/
resize: function resize(r, deltaWidth, deltaHeight) {
r = this.normalize(r);
deltaWidth = deltaWidth || 0;
deltaHeight = deltaHeight || 0;
return this.normalize({
left: r.left,
right: r.right + deltaWidth,
top: r.top,
bottom: r.bottom + deltaHeight,
width: r.width + deltaWidth,
height: r.height + deltaHeight
});
},
/**
* Resizes a rect with a delta width (changes width and right)
*
* @param r
* @param deltaWidth
* @return {undefined}
*/
resizeWidth: function resizeWidth(r, deltaWidth) {
return this.resize(r, deltaWidth, 0);
},
/**
* Resizes a rect with a delta height (changes height and bottom)
*
* @param r
* @param deltaHeight
* @return {undefined}
*/
resizeHeight: function resizeHeight(r, deltaHeight) {
return this.resize(r, 0, deltaHeight);
},
/**
* Resizes a rect to an absolute width and height (not delta values)
*
* @param r
* @param width
* @param height
* @return {undefined}
*/
resizeTo: function resizeTo(r, width, height) {
r = this.normalize(r);
width = width || r.width;
height = height || r.height;
return this.normalize({
left: r.left,
right: r.left + width,
top: r.top,
bottom: r.top + height,
width: width,
height: height
});
},
/**
* Resizes a rect to an absolute width (not delta value)
*
* @param r
* @param width
* @return {undefined}
*/
resizeToWidth: function resizeToWidth(r, width) {
return this.resizeTo(r, width, null);
},
/**
* Resizes to rect to an absolute height (not delta value)
*
* @param r
* @param height
* @return {undefined}
*/
resizeToHeight: function resizeToHeight(r, height) {
return this.resizeTo(r, null, height);
},
/**
* Moves a rect by adding the specified x and y values to left and top.
*
* The width and height are not changed, but the right and bottom values are changed based
* on the new left/top and current width/height.
*
* This does not change width and height;
*
* @param {!Object} r - rectangle-like object
* @param {?number} [x=0] - delta value for left
* @param {?number} [y=0] - delta value for top
* @return {Object} - resulting normalized rectangle object
*/
move: function move(r, deltaX, deltaY) {
r = this.normalize(r);
deltaX = deltaX || 0;
deltaY = deltaY || 0;
return this.normalize({
left: r.left + deltaX,
right: r.right + deltaX,
top: r.top + deltaY,
bottom: r.bottom + deltaY,
width: r.width,
height: r.height
});
},
/**
* Moves a rect by a delta x value.
*
* @param r
* @param deltaX
* @return {undefined}
*/
moveX: function moveX(r, deltaX) {
return this.move(r, deltaX, 0);
},
/**
* Moves a rect by a delta y value.
*
* @param r
* @param deltaY
* @return {undefined}
*/
moveY: function moveY(r, deltaY) {
return this.move(r, 0, deltaY);
},
/**
* Moves a rect to an absolute x and y location.
*
* @param r
* @param x
* @param y
* @return {undefined}
*/
moveTo: function moveTo(r, x, y) {
r = this.normalize(r);
x = x || r.left;
y = y || r.top;
return this.normalize({
left: x,
right: x + r.width,
top: y,
bottom: y + r.height,
width: r.width,
height: r.height
});
},
/**
* Moves a rect to an absolute x location.
*
* @param r
* @param x
* @return {undefined}
*/
moveToX: function moveToX(r, x) {
return this.moveTo(r, x, null);
},
/**
* Moves a rect to an absolute y location.
*
* @param r
* @param y
* @return {undefined}
*/
moveToY: function moveToY(r, y) {
return this.moveTo(r, null, y);
},
/**
* Calculates the distance between two points. Points can be expressed with top and left values,
* or x and y values.
*
* @param {!Object} point1 - first point
* @param {!Object} point2 - second point
* @return {number} - the distance between the points
*/
distance: function distance(point1, point2) {
var x1 = point1.left || point1.x || 0;
var y1 = point1.top || point1.y || 0;
var x2 = point2.left || point2.x || 0;
var y2 = point2.top || point2.y || 0;
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
};