UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

449 lines (400 loc) 11.9 kB
/** * Implement a simple 2d rectangle. * * |-- copyright and license --| * @module Shaku * @file shaku\src\utils\rectangle.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const Circle = require('./circle'); const Line = require('./line'); const MathHelper = require('./math_helper'); const Vector2 = require('./vector2'); /** * Implement a simple 2d Rectangle. */ class Rectangle { /** * Create the Rect. * @param {Number} x Rect position X (top left corner). * @param {Number} y Rect position Y (top left corner). * @param {Number} width Rect width. * @param {Number} height Rect height. */ constructor(x, y, width, height) { this.x = x || 0; this.y = y || 0; this.width = width; this.height = height; } /** * Set rectangle values. * @param {Number} x Rectangle x position. * @param {Number} y Rectangle y position. * @param {Number} width Rectangle width. * @param {Number} height Rectangle height. * @returns {Rectangle} this. */ set(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; return this; } /** * Copy another rectangle. * @param {other} other Rectangle to copy. * @returns {Rectangle} this. */ copy(other) { this.x = other.x; this.y = other.y; this.width = other.width; this.height = other.height; return this; } /** * Get position as Vector2. * @returns {Vector2} Position vector. */ getPosition() { return new Vector2(this.x, this.y); } /** * Get size as Vector2. * @returns {Vector2} Size vector. */ getSize() { return new Vector2(this.width, this.height); } /** * Get center position. * @returns {Vector2} Position vector. */ getCenter() { return new Vector2(Math.round(this.x + this.width / 2), Math.round(this.y + this.height / 2)); } /** * Get left value. * @returns {Number} rectangle left. */ get left() { return this.x; } /** * Get right value. * @returns {Number} rectangle right. */ get right() { return this.x + this.width; } /** * Get top value. * @returns {Number} rectangle top. */ get top() { return this.y; } /** * Get bottom value. * @returns {Number} rectangle bottom. */ get bottom() { return this.y + this.height; } /** * Return a clone of this rectangle. * @returns {Rectangle} Cloned rectangle. */ clone() { return new Rectangle(this.x, this.y, this.width, this.height); } /** * Get top-left corner. * @returns {Vector2} Corner position vector. */ getTopLeft() { return new Vector2(this.x, this.y); } /** * Get top-right corner. * @returns {Vector2} Corner position vector. */ getTopRight() { return new Vector2(this.x + this.width, this.y); } /** * Get bottom-left corner. * @returns {Vector2} Corner position vector. */ getBottomLeft() { return new Vector2(this.x, this.y + this.height); } /** * Get bottom-right corner. * @returns {Vector2} Corner position vector. */ getBottomRight() { return new Vector2(this.x + this.width, this.y + this.height); } /** * Convert to string. */ string() { return this.x + ',' + this.y + ',' + this.width + ',' + this.height; } /** * Check if this rectangle contains a Vector2. * @param {Vector2} p Point to check. * @returns {Boolean} if point is contained within the rectangle. */ containsVector(p) { return (p.x >= this.x) && (p.x <= this.x + this.width) && (p.y >= this.y) && (p.y <= this.y + this.height); } /** * Check if this rectangle collides with another rectangle. * @param {Rectangle} other Rectangle to check collision with. * @return {Boolean} if rectangles collide. */ collideRect(other) { let r1 = this; let r2 = other; return !(r2.left >= r1.right || r2.right <= r1.left || r2.top >= r1.bottom || r2.bottom <= r1.top); } /** * Check if this rectangle collides with a line. * @param {Line} line Line to check collision with. * @return {Boolean} if rectangle collides with line. */ collideLine(line) { // first check if rectangle contains any of the line points if (this.containsVector(line.from) || this.containsVector(line.to)) { return true; } // now check intersection with the rectangle lines let topLeft = this.getTopLeft(); let topRight = this.getTopRight(); let bottomLeft = this.getBottomLeft(); let bottomRight = this.getBottomRight(); if (line.collideLine(new Line(topLeft, topRight))) { return true; } if (line.collideLine(new Line(topLeft, bottomLeft))) { return true; } if (line.collideLine(new Line(topRight, bottomRight))) { return true; } if (line.collideLine(new Line(bottomLeft, bottomRight))) { return true; } // no collision return false; } /** * Checks if this rectangle collides with a circle. * @param {Circle} circle Circle to check collision with. * @return {Boolean} if rectangle collides with circle. */ collideCircle(circle) { // get center and radius let center = circle.center; let radius = circle.radius; // first check if circle center is inside the rectangle - easy case let rect = this; if (rect.containsVector(center)) { return true; } // get rectangle center let rectCenter = rect.getCenter(); // get corners let topLeft = rect.getTopLeft(); let topRight = rect.getTopRight(); let bottomRight = rect.getBottomRight(); let bottomLeft = rect.getBottomLeft(); // create a list of lines to check (in the rectangle) based on circle position to rect center let lines = []; if (rectCenter.x > center.x) { lines.push([topLeft, bottomLeft]); } else { lines.push([topRight, bottomRight]); } if (rectCenter.y > center.y) { lines.push([topLeft, topRight]); } else { lines.push([bottomLeft, bottomRight]); } // now check intersection between circle and each of the rectangle lines for (let i = 0; i < lines.length; ++i) { let disToLine = pointLineDistance(center, lines[i][0], lines[i][1]); if (disToLine <= radius) { return true; } } // no collision.. return false; } /** * Get the smallest circle containing this rectangle. * @returns {Circle} Bounding circle. */ getBoundingCircle() { let center = this.getCenter(); let radius = center.distanceTo(this.getTopLeft()); return new Circle(center, radius); } /** * Build and return a rectangle from points. * @param {Array<Vector2>} points Points to build rectangle from. * @returns {Rectangle} new rectangle from points. */ static fromPoints(points) { let min_x = points[0].x; let min_y = points[0].y; let max_x = min_x; let max_y = min_y; for (let i = 1; i < points.length; ++i) { min_x = Math.min(min_x, points[i].x); min_y = Math.min(min_y, points[i].y); max_x = Math.max(max_x, points[i].x); max_y = Math.max(max_y, points[i].y); } return new Rectangle(min_x, min_y, max_x - min_x, max_y - min_y); } /** * Return a resized rectangle with the same center point. * @param {Number|Vector2} amount Amount to resize. * @returns {Rectangle} resized rectangle. */ resize(amount) { if (typeof amount === 'number') { amount = new Vector2(amount, amount); } return new Rectangle(this.x - amount.x / 2, this.y - amount.y / 2, this.width + amount.x, this.height + amount.y); } /** * Check if equal to another rectangle. * @param {Rectangle} other Other rectangle to compare to. */ equals(other) { return (this === other) || (other && (other.constructor === this.constructor) && (this.x == other.x) && (this.y == other.y) && (this.width == other.width) && (this.height == other.height)); } /** * Lerp between two rectangles. * @param {Rectangle} p1 First rectangles. * @param {Rectangle} p2 Second rectangles. * @param {Number} a Lerp factor (0.0 - 1.0). * @returns {Rectangle} result rectangle. */ static lerp(p1, p2, a) { let lerpScalar = MathHelper.lerp; return new Rectangle( lerpScalar(p1.x, p2.x, a), lerpScalar(p1.y, p2.y, a), lerpScalar(p1.width, p2.width, a), lerpScalar(p1.height, p2.height, a) ); } /** * Create rectangle from a dictionary. * @param {*} data Dictionary with {x,y,width,height}. * @returns {Rectangle} Newly created rectangle. */ static fromDict(data) { return new Rectangle(data.x || 0, data.y || 0, data.width || 0, data.height || 0); } /** * Convert to dictionary. * @param {Boolean} minimized If true, will not include keys that their values are 0. You can use fromDict on minimized dicts. * @returns {*} Dictionary with {x,y,width,height} */ toDict(minimized) { if (minimized) { const ret = {}; if (this.x) { ret.x = this.x; } if (this.y) { ret.y = this.y; } if (this.width) { ret.width = this.width; } if (this.height) { ret.height = this.height; } return ret; } return {x: this.x, y: this.y, width: this.width, height: this.height}; } } /** * Get distance between a point and a line. * @private */ function pointLineDistance(p1, l1, l2) { let x = p1.x; let y = p1.y; let x1 = l1.x; let y1 = l1.y; let x2 = l2.x; let y2 = l2.y; var A = x - x1; var B = y - y1; var C = x2 - x1; var D = y2 - y1; var dot = A * C + B * D; var len_sq = C * C + D * D; var param = -1; if (len_sq != 0) //in case of 0 length line param = dot / len_sq; var xx, yy; if (param < 0) { xx = x1; yy = y1; } else if (param > 1) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } var dx = x - xx; var dy = y - yy; return Math.sqrt(dx * dx + dy * dy); } // export the rectangle class module.exports = Rectangle;