shaku
Version:
A simple and effective JavaScript game development framework that knows its place!
449 lines (400 loc) • 11.9 kB
JavaScript
/**
* 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;