UNPKG

shaku

Version:

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

808 lines (733 loc) 21.2 kB
/** * Implement a simple 2d vector. * * |-- copyright and license --| * @module Shaku * @file shaku\src\utils\vector2.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const MathHelper = require("./math_helper"); /** * A simple Vector object for 2d positions. */ class Vector2 { /** * Create the Vector object. * @param {number} x Vector X. * @param {number} y Vector Y. */ constructor(x = 0, y = 0) { this.x = x; this.y = y; } /** * Clone the vector. * @returns {Vector2} cloned vector. */ clone() { return new Vector2(this.x, this.y); } /** * Set vector value. * @param {Number} x X component. * @param {Number} y Y component. * @returns {Vector2} this. */ set(x, y) { this.x = x; this.y = y; return this; } /** * Copy values from other vector into self. * @returns {Vector2} this. */ copy(other) { this.x = other.x; this.y = other.y; return this; } /** * Return a new vector of this + other. * @param {Number|Vector2} Other Vector3 or number to add to all components. * @returns {Vector2} result vector. */ add(other) { if (typeof other === 'number') { return new Vector2(this.x + other, this.y + (arguments[1] === undefined ? other : arguments[1])); } return new Vector2(this.x + other.x, this.y + other.y); } /** * Return a new vector of this - other. * @param {Number|Vector2} Other Vector3 or number to sub from all components. * @returns {Vector2} result vector. */ sub(other) { if (typeof other === 'number') { return new Vector2(this.x - other, this.y - (arguments[1] === undefined ? other : arguments[1])); } return new Vector2(this.x - other.x, this.y - other.y); } /** * Return a new vector of this / other. * @param {Number|Vector2} Other Vector3 or number to divide by all components. * @returns {Vector2} result vector. */ div(other) { if (typeof other === 'number') { return new Vector2(this.x / other, this.y / (arguments[1] === undefined ? other : arguments[1])); } return new Vector2(this.x / other.x, this.y / other.y); } /** * Return a new vector of this * other. * @param {Number|Vector2} Other Vector2 or number to multiply with all components. * @returns {Vector2} result vector. */ mul(other) { if (typeof other === 'number') { return new Vector2(this.x * other, this.y * (arguments[1] === undefined ? other : arguments[1])); } return new Vector2(this.x * other.x, this.y * other.y); } /** * Return a round copy of this vector. * @returns {Vector2} result vector. */ round() { return new Vector2(Math.round(this.x), Math.round(this.y)); } /** * Return a floored copy of this vector. * @returns {Vector2} result vector. */ floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); } /** * Return a ceiled copy of this vector. * @returns {Vector2} result vector. */ ceil() { return new Vector2(Math.ceil(this.x), Math.ceil(this.y)); } /** * Set self values to be min values between self and a given vector. * @param {Vector2} v Vector to min with. * @returns {Vector2} Self. */ minSelf(v) { this.x = Math.min( this.x, v.x ); this.y = Math.min( this.y, v.y ); return this; } /** * Set self values to be max values between self and a given vector. * @param {Vector2} v Vector to max with. * @returns {Vector2} Self. */ maxSelf(v) { this.x = Math.max( this.x, v.x ); this.y = Math.max( this.y, v.y ); return this; } /** * Create a clone vector that is the min result between self and a given vector. * @param {Vector2} v Vector to min with. * @returns {Vector2} Result vector. */ min(v) { this.clone().minSelf(v); } /** * Create a clone vector that is the max result between self and a given vector. * @param {Vector2} v Vector to max with. * @returns {Vector2} Result vector. */ max(v) { this.clone().maxSelf(v); } /** * Return a normalized copy of this vector. * @returns {Vector2} result vector. */ normalized() { if (this.x == 0 && this.y == 0) { return Vector2.zero(); } let mag = this.length(); return new Vector2(this.x / mag, this.y / mag); } /** * Get a copy of this vector rotated by radians. * @param {Number} radians Radians to rotate by. * @returns {Vector2} New vector with the length of this vector and direction rotated by given radians. */ rotatedByRadians(radians) { return Vector2.fromRadians(this.getRadians() + radians).mulSelf(this.length()); } /** * Get a copy of this vector rotated by degrees. * @param {Number} degrees Degrees to rotate by. * @returns {Vector2} New vector with the length of this vector and direction rotated by given degrees. */ rotatedByDegrees(degrees) { return Vector2.fromDegrees(this.getDegrees() + degrees).mulSelf(this.length()); } /** * Add other vector values to self. * @param {Number|Vector2} Other Vector or number to add. * @returns {Vector2} this. */ addSelf(other) { if (typeof other === 'number') { this.x += other; this.y += (arguments[1] === undefined ? other : arguments[1]); } else { this.x += other.x; this.y += other.y; } return this; } /** * Sub other vector values from self. * @param {Number|Vector2} Other Vector or number to substract. * @returns {Vector2} this. */ subSelf(other) { if (typeof other === 'number') { this.x -= other; this.y -= (arguments[1] === undefined ? other : arguments[1]); } else { this.x -= other.x; this.y -= other.y; } return this; } /** * Divide this vector by other vector values. * @param {Number|Vector2} Other Vector or number to divide by. * @returns {Vector2} this. */ divSelf(other) { if (typeof other === 'number') { this.x /= other; this.y /= (arguments[1] === undefined ? other : arguments[1]); } else { this.x /= other.x; this.y /= other.y; } return this; } /** * Multiply this vector by other vector values. * @param {Number|Vector2} Other Vector or number to multiply by. * @returns {Vector2} this. */ mulSelf(other) { if (typeof other === 'number') { this.x *= other; this.y *= (arguments[1] === undefined ? other : arguments[1]); } else { this.x *= other.x; this.y *= other.y; } return this; } /** * Round self. * @returns {Vector2} this. */ roundSelf() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; } /** * Floor self. * @returns {Vector2} this. */ floorSelf() { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; } /** * Ceil self. * @returns {Vector2} this. */ ceilSelf() { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; } /** * Return a normalized copy of this vector. * @returns {Vector2} this. */ normalizeSelf() { if (this.x == 0 && this.y == 0) { return this; } let mag = this.length(); this.x /= mag; this.y /= mag; return this; } /** * Return if vector equals another vector. * @param {Vector2} other Other vector to compare to. * @returns {Boolean} if vectors are equal. */ equals(other) { return ((this === other) || ((other.constructor === this.constructor) && this.x === other.x && this.y === other.y)); } /** * Return if vector approximately equals another vector. * @param {Vector2} other Other vector to compare to. * @param {Number} threshold Distance threshold to consider as equal. Defaults to 1. * @returns {Boolean} if vectors are equal. */ approximate(other, threshold) { threshold = threshold || 1; return ((this === other) || ( (Math.abs(this.x - other.x) <= threshold) && (Math.abs(this.y - other.y) <= threshold)) ); } /** * Return vector length (aka magnitude). * @returns {Number} Vector length. */ length() { return Math.sqrt((this.x * this.x) + (this.y * this.y)); } /** * Return a copy of this vector multiplied by a factor. * @returns {Vector2} result vector. */ scaled(fac) { return new Vector2(this.x * fac, this.y * fac); } /** * Get vector (0,0). * @returns {Vector2} result vector. */ static zero() { return new Vector2(0, 0); } /** * Get vector with 1,1 values. * @returns {Vector2} result vector. */ static one() { return new Vector2(1, 1); } /** * Get vector with 0.5,0.5 values. * @returns {Vector2} result vector. */ static half() { return new Vector2(0.5, 0.5); } /** * Get vector with -1,0 values. * @returns {Vector2} result vector. */ static left() { return new Vector2(-1, 0); } /** * Get vector with 1,0 values. * @returns {Vector2} result vector. */ static right() { return new Vector2(1, 0); } /** * Get vector with 0,-1 values. * @returns {Vector2} result vector. */ static up() { return new Vector2(0, -1); } /** * Get vector with 0,1 values. * @returns {Vector2} result vector. */ static down() { return new Vector2(0, 1); } /** * Get a random vector with length of 1. * @returns {Vector2} result vector. */ static random() { return Vector2.fromDegrees(Math.random() * 360); } /** * Get degrees between this vector and another vector. * @param {Vector2} other Other vector. * @returns {Number} Angle between vectors in degrees. */ degreesTo(other) { return Vector2.degreesBetween(this, other); }; /** * Get radians between this vector and another vector. * @param {Vector2} other Other vector. * @returns {Number} Angle between vectors in radians. */ radiansTo(other) { return Vector2.radiansBetween(this, other); }; /** * Get degrees between this vector and another vector. * Return values between 0 to 360. * @param {Vector2} other Other vector. * @returns {Number} Angle between vectors in degrees. */ wrappedDegreesTo(other) { return Vector2.wrappedDegreesBetween(this, other); }; /** * Get radians between this vector and another vector. * Return values between 0 to PI2. * @param {Vector2} other Other vector. * @returns {Number} Angle between vectors in radians. */ wrappedRadiansTo(other) { return Vector2.wrappedRadiansBetween(this, other); }; /** * Calculate distance between this vector and another vectors. * @param {Vector2} other Other vector. * @returns {Number} Distance between vectors. */ distanceTo(other) { return Vector2.distance(this, other); } /** * Calculate squared distance between this vector and another vector. * @param {Vector2} other Other vector. * @returns {Number} Distance between vectors. */ distanceToSquared(other) { const dx = this.x - other.x, dy = this.y - other.y; return dx * dx + dy * dy; } /** * Return a clone and clamp its values to be between min and max. * @param {Vector2} min Min vector. * @param {Vector2} max Max vector. * @returns {Vector2} Clamped vector. */ clamp( min, max ) { return this.clone().clampSelf(min, max); } /** * Clamp this vector values to be between min and max. * @param {Vector2} min Min vector. * @param {Vector2} max Max vector. * @returns {Vector2} Self. */ clampSelf( min, max ) { this.x = Math.max( min.x, Math.min( max.x, this.x ) ); this.y = Math.max( min.y, Math.min( max.y, this.y ) ); return this; } /** * Calculate the dot product with another vector. * @param {Vector2} other Vector to calculate dot with. * @returns {Number} Dot product value. */ dot(other) { return this.x * other.x + this.y * other.y; } /** * Get vector from degrees. * @param {Number} degrees Angle to create vector from (0 = vector pointing right). * @returns {Vector2} result vector. */ static fromDegrees(degrees) { let rads = degrees * (Math.PI / 180); return new Vector2(Math.cos(rads), Math.sin(rads)); } /** * Get vector from radians. * @param {Number} radians Angle to create vector from (0 = vector pointing right). * @returns {Vector2} result vector. */ static fromRadians(radians) { return new Vector2(Math.cos(radians), Math.sin(radians)); } /** * Lerp between two vectors. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @param {Number} a Lerp factor (0.0 - 1.0). * @returns {Vector2} result vector. */ static lerp(p1, p2, a) { let lerpScalar = MathHelper.lerp; return new Vector2(lerpScalar(p1.x, p2.x, a), lerpScalar(p1.y, p2.y, a)); } /** * Get degrees between two vectors. * Return values between -180 to 180. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Angle between vectors in degrees. */ static degreesBetween(P1, P2) { let deltaY = P2.y - P1.y, deltaX = P2.x - P1.x; return Math.atan2(deltaY, deltaX) * (180 / Math.PI); }; /** * Get radians between two vectors. * Return values between -PI to PI. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Angle between vectors in radians. */ static radiansBetween(P1, P2) { return MathHelper.toRadians(Vector2.degreesBetween(P1, P2)); }; /** * Get degrees between two vectors. * Return values between 0 to 360. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Angle between vectors in degrees. */ static wrappedDegreesBetween(P1, P2) { let temp = P2.sub(P1); return temp.getDegrees(); }; /** * Get vector's angle in degrees. * @returns {Number} Vector angle in degrees. */ getDegrees() { var angle = Math.atan2(this.y, this.x); var degrees = 180 * angle / Math.PI; return (360+Math.round(degrees)) % 360; } /** * Get vector's angle in radians. * @returns {Number} Vector angle in degrees. */ getRadians() { var angle = Math.atan2(this.y, this.x); return angle; } /** * Get radians between two vectors. * Return values between 0 to PI2. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Angle between vectors in radians. */ static wrappedRadiansBetween(P1, P2) { return MathHelper.toRadians(Vector2.wrappedDegreesBetween(P1, P2)); }; /** * Calculate distance between two vectors. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Distance between vectors. */ static distance(p1, p2) { let a = p1.x - p2.x; let b = p1.y - p2.y; return Math.sqrt(a*a + b*b); } /** * Return cross product between two vectors. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Cross between vectors. */ static cross(p1, p2) { return p1.x * p2.y - p1.y * p2.x; } /** * Return dot product between two vectors. * @param {Vector2} p1 First vector. * @param {Vector2} p2 Second vector. * @returns {Number} Dot between vectors. */ static dot(p1, p2) { return p1.x * p2.x + p1.y * p2.y; } /** * Convert to string. */ string() { return this.x + ',' + this.y; } /** * Parse and return a vector object from string in the form of "x,y". * @param {String} str String to parse. * @returns {Vector2} Parsed vector. */ static parse(str) { let parts = str.split(','); return new Vector2(parseFloat(parts[0].trim()), parseFloat(parts[1].trim())); } /** * Convert to array of numbers. * @returns {Array<Number>} Vector components as array. */ toArray() { return [this.x, this.y]; } /** * Create vector from array of numbers. * @param {Array<Number>} arr Array of numbers to create vector from. * @returns {Vector2} Vector instance. */ static fromArray(arr) { return new Vector2(arr[0], arr[1]); } /** * Create vector from a dictionary. * @param {*} data Dictionary with {x,y}. * @returns {Vector2} Newly created vector. */ static fromDict(data) { return new Vector2(data.x || 0, data.y || 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} */ toDict(minimized) { if (minimized) { const ret = {}; if (this.x) { ret.x = this.x; } if (this.y) { ret.y = this.y; } return ret; } return {x: this.x, y: this.y}; } } /** * Vector with 0,0 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.zeroReadonly = new Vector2(0, 0); Object.freeze(Vector2.zeroReadonly); /** * Vector with 1,1 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.oneReadonly = new Vector2(1, 1); Object.freeze(Vector2.oneReadonly); /** * Vector with 0.5,0.5 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.halfReadonly = new Vector2(0.5, 0.5); Object.freeze(Vector2.halfReadonly); /** * Vector with -1,0 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.leftReadonly = new Vector2(-1, 0); Object.freeze(Vector2.leftReadonly); /** * Vector with 1,0 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.rightReadonly = new Vector2(1, 0); Object.freeze(Vector2.rightReadonly); /** * Vector with 0,1 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.upReadonly = new Vector2(0, -1); Object.freeze(Vector2.upReadonly); /** * Vector with 0,-1 values as a frozen shared object. * Be careful not to try and change it. */ Vector2.downReadonly = new Vector2(0, 1); Object.freeze(Vector2.downReadonly); // export vector object module.exports = Vector2;