import { globalConfig } from "./config"; import { epsilonCompare, round2Digits } from "./utils"; import { Vector } from "./vector"; export class Rectangle { constructor(x = 0, y = 0, w = 0, h = 0) { this.x = x; this.y = y; this.w = w; this.h = h; } /** * Creates a rectangle from top right bottom and left offsets * @param {number} top * @param {number} right * @param {number} bottom * @param {number} left */ static fromTRBL(top, right, bottom, left) { return new Rectangle(left, top, right - left, bottom - top); } /** * Constructs a new square rectangle * @param {number} x * @param {number} y * @param {number} size */ static fromSquare(x, y, size) { return new Rectangle(x, y, size, size); } /** * * @param {Vector} p1 * @param {Vector} p2 */ static fromTwoPoints(p1, p2) { const left = Math.min(p1.x, p2.x); const top = Math.min(p1.y, p2.y); const right = Math.max(p1.x, p2.x); const bottom = Math.max(p1.y, p2.y); return new Rectangle(left, top, right - left, bottom - top); } /** * * @param {number} width * @param {number} height */ static centered(width, height) { return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height); } /** * Returns if a intersects b * @param {Rectangle} a * @param {Rectangle} b */ static intersects(a, b) { return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom; } /** * Copies this instance * @returns {Rectangle} */ clone() { return new Rectangle(this.x, this.y, this.w, this.h); } /** * Returns if this rectangle is empty * @returns {boolean} */ isEmpty() { return epsilonCompare(this.w * this.h, 0); } /** * Returns if this rectangle is equal to the other while taking an epsilon into account * @param {Rectangle} other * @param {number} [epsilon] */ equalsEpsilon(other, epsilon) { return ( epsilonCompare(this.x, other.x, epsilon) && epsilonCompare(this.y, other.y, epsilon) && epsilonCompare(this.w, other.w, epsilon) && epsilonCompare(this.h, other.h, epsilon) ); } /** * @returns {number} */ left() { return this.x; } /** * @returns {number} */ right() { return this.x + this.w; } /** * @returns {number} */ top() { return this.y; } /** * @returns {number} */ bottom() { return this.y + this.h; } /** * Returns Top, Right, Bottom, Left * @returns {[number, number, number, number]} */ trbl() { return [this.y, this.right(), this.bottom(), this.x]; } /** * Returns the center of the rect * @returns {Vector} */ getCenter() { return new Vector(this.x + this.w / 2, this.y + this.h / 2); } /** * Sets the right side of the rect without moving it * @param {number} right */ setRight(right) { this.w = right - this.x; } /** * Sets the bottom side of the rect without moving it * @param {number} bottom */ setBottom(bottom) { this.h = bottom - this.y; } /** * Sets the top side of the rect without scaling it * @param {number} top */ setTop(top) { const bottom = this.bottom(); this.y = top; this.setBottom(bottom); } /** * Sets the left side of the rect without scaling it * @param {number} left */ setLeft(left) { const right = this.right(); this.x = left; this.setRight(right); } /** * Returns the top left point * @returns {Vector} */ topLeft() { return new Vector(this.x, this.y); } /** * Returns the bottom left point * @returns {Vector} */ bottomRight() { return new Vector(this.right(), this.bottom()); } /** * Moves the rectangle by the given parameters * @param {number} x * @param {number} y */ moveBy(x, y) { this.x += x; this.y += y; } /** * Moves the rectangle by the given vector * @param {Vector} vec */ moveByVector(vec) { this.x += vec.x; this.y += vec.y; } /** * Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to * tile space and vice versa * @param {number} factor */ allScaled(factor) { return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor); } /** * Expands the rectangle in all directions * @param {number} amount * @returns {Rectangle} new rectangle */ expandedInAllDirections(amount) { return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount); } /** * Returns if the given rectangle is contained * @param {Rectangle} rect * @returns {boolean} */ containsRect(rect) { return ( this.x <= rect.right() && rect.x <= this.right() && this.y <= rect.bottom() && rect.y <= this.bottom() ); } /** * Returns if this rectangle contains the other rectangle specified by the parameters * @param {number} x * @param {number} y * @param {number} w * @param {number} h * @returns {boolean} */ containsRect4Params(x, y, w, h) { return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom(); } /** * Returns if the rectangle contains the given circle at (x, y) with the radius (radius) * @param {number} x * @param {number} y * @param {number} radius * @returns {boolean} */ containsCircle(x, y, radius) { return ( this.x <= x + radius && x - radius <= this.right() && this.y <= y + radius && y - radius <= this.bottom() ); } /** * Returns if hte rectangle contains the given point * @param {number} x * @param {number} y * @returns {boolean} */ containsPoint(x, y) { return x >= this.x && x < this.right() && y >= this.y && y < this.bottom(); } /** * Returns the shared area with another rectangle, or null if there is no intersection * @param {Rectangle} rect * @returns {Rectangle|null} */ getIntersection(rect) { const left = Math.max(this.x, rect.x); const top = Math.max(this.y, rect.y); const right = Math.min(this.x + this.w, rect.x + rect.w); const bottom = Math.min(this.y + this.h, rect.y + rect.h); if (right <= left || bottom <= top) { return null; } return Rectangle.fromTRBL(top, right, bottom, left); } /** * Returns whether the rectangle fully intersects the given rectangle * @param {Rectangle} rect */ intersectsFully(rect) { const intersection = this.getIntersection(rect); return intersection && Math.abs(intersection.w * intersection.h - rect.w * rect.h) < 0.001; } /** * Returns the union of this rectangle with another * @param {Rectangle} rect */ getUnion(rect) { if (this.isEmpty()) { // If this is rect is empty, return the other one return rect.clone(); } if (rect.isEmpty()) { // If the other is empty, return this one return this.clone(); } // Find contained area const left = Math.min(this.x, rect.x); const top = Math.min(this.y, rect.y); const right = Math.max(this.right(), rect.right()); const bottom = Math.max(this.bottom(), rect.bottom()); return Rectangle.fromTRBL(top, right, bottom, left); } /** * Good for caching stuff */ toCompareableString() { return ( round2Digits(this.x) + "/" + round2Digits(this.y) + "/" + round2Digits(this.w) + "/" + round2Digits(this.h) ); } /** * Good for printing stuff */ toString() { return ( "[x:" + round2Digits(this.x) + "| y:" + round2Digits(this.y) + "| w:" + round2Digits(this.w) + "| h:" + round2Digits(this.h) + "]" ); } /** * Returns a new rectangle in tile space which includes all tiles which are visible in this rect * @returns {Rectangle} */ toTileCullRectangle() { return new Rectangle( Math.floor(this.x / globalConfig.tileSize), Math.floor(this.y / globalConfig.tileSize), Math.ceil(this.w / globalConfig.tileSize), Math.ceil(this.h / globalConfig.tileSize) ); } }