You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tobspr_shapez.io/src/js/core/vector.js

691 lines
17 KiB

import { globalConfig } from "./config";
import { safeModulo } from "./utils";
const tileSize = globalConfig.tileSize;
const halfTileSize = globalConfig.halfTileSize;
/**
* @enum {string}
*/
export const enumDirection = {
top: "top",
right: "right",
bottom: "bottom",
left: "left",
};
/**
* @enum {string}
*/
export const enumInvertedDirections = {
[enumDirection.top]: enumDirection.bottom,
[enumDirection.right]: enumDirection.left,
[enumDirection.bottom]: enumDirection.top,
[enumDirection.left]: enumDirection.right,
};
/**
* @enum {number}
*/
export const enumDirectionToAngle = {
[enumDirection.top]: 0,
[enumDirection.right]: 90,
[enumDirection.bottom]: 180,
[enumDirection.left]: 270,
};
/**
* @enum {enumDirection}
*/
export const enumAngleToDirection = {
0: enumDirection.top,
90: enumDirection.right,
180: enumDirection.bottom,
270: enumDirection.left,
};
/** @type {Array<enumDirection>} */
export const arrayAllDirections = [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
];
export class Vector {
/**
*
* @param {number=} x
* @param {number=} y
*/
constructor(x, y) {
this.x = x || 0;
this.y = y || 0;
}
/**
* return a copy of the vector
* @returns {Vector}
*/
copy() {
return new Vector(this.x, this.y);
}
/**
* Adds a vector and return a new vector
* @param {Vector} other
* @returns {Vector}
*/
add(other) {
return new Vector(this.x + other.x, this.y + other.y);
}
/**
* Adds a vector
* @param {Vector} other
* @returns {Vector}
*/
addInplace(other) {
this.x += other.x;
this.y += other.y;
return this;
}
/**
* Substracts a vector and return a new vector
* @param {Vector} other
* @returns {Vector}
*/
sub(other) {
return new Vector(this.x - other.x, this.y - other.y);
}
/**
* Subs a vector
* @param {Vector} other
* @returns {Vector}
*/
subInplace(other) {
this.x -= other.x;
this.y -= other.y;
return this;
}
/**
* Multiplies with a vector and return a new vector
* @param {Vector} other
* @returns {Vector}
*/
mul(other) {
return new Vector(this.x * other.x, this.y * other.y);
}
/**
* Adds two scalars and return a new vector
* @param {number} x
* @param {number} y
* @returns {Vector}
*/
addScalars(x, y) {
return new Vector(this.x + x, this.y + y);
}
/**
* Substracts a scalar and return a new vector
* @param {number} f
* @returns {Vector}
*/
subScalar(f) {
return new Vector(this.x - f, this.y - f);
}
/**
* Substracts two scalars and return a new vector
* @param {number} x
* @param {number} y
* @returns {Vector}
*/
subScalars(x, y) {
return new Vector(this.x - x, this.y - y);
}
/**
* Returns the euclidian length
* @returns {number}
*/
length() {
return Math.hypot(this.x, this.y);
}
/**
* Returns the square length
* @returns {number}
*/
lengthSquare() {
return this.x * this.x + this.y * this.y;
}
/**
* Divides both components by a scalar and return a new vector
* @param {number} f
* @returns {Vector}
*/
divideScalar(f) {
return new Vector(this.x / f, this.y / f);
}
/**
* Divides both components by the given scalars and return a new vector
* @param {number} a
* @param {number} b
* @returns {Vector}
*/
divideScalars(a, b) {
return new Vector(this.x / a, this.y / b);
}
/**
* Divides both components by a scalar
* @param {number} f
* @returns {Vector}
*/
divideScalarInplace(f) {
this.x /= f;
this.y /= f;
return this;
}
/**
* Multiplies both components with a scalar and return a new vector
* @param {number} f
* @returns {Vector}
*/
multiplyScalar(f) {
return new Vector(this.x * f, this.y * f);
}
/**
* Multiplies both components with two scalars and returns a new vector
* @param {number} a
* @param {number} b
* @returns {Vector}
*/
multiplyScalars(a, b) {
return new Vector(this.x * a, this.y * b);
}
/**
* For both components, compute the maximum of each component and the given scalar, and return a new vector.
* For example:
* - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
* @param {number} f
* @returns {Vector}
*/
maxScalar(f) {
return new Vector(Math.max(f, this.x), Math.max(f, this.y));
}
/**
* Adds a scalar to both components and return a new vector
* @param {number} f
* @returns {Vector}
*/
addScalar(f) {
return new Vector(this.x + f, this.y + f);
}
/**
* Computes the component wise minimum and return a new vector
* @param {Vector} v
* @returns {Vector}
*/
min(v) {
return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y));
}
/**
* Computes the component wise maximum and return a new vector
* @param {Vector} v
* @returns {Vector}
*/
max(v) {
return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
}
/**
* Computes the component wise absolute
* @returns {Vector}
*/
abs() {
return new Vector(Math.abs(this.x), Math.abs(this.y));
}
/**
* Computes the scalar product
* @param {Vector} v
* @returns {number}
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
* Computes the distance to a given vector
* @param {Vector} v
* @returns {number}
*/
distance(v) {
return Math.hypot(this.x - v.x, this.y - v.y);
}
/**
* Computes the square distance to a given vectort
* @param {Vector} v
* @returns {number}
*/
distanceSquare(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return dx * dx + dy * dy;
}
/**
* Returns x % f, y % f
* @param {number} f
* @returns {Vector} new vector
*/
modScalar(f) {
return new Vector(safeModulo(this.x, f), safeModulo(this.y, f));
}
/**
* Computes and returns the center between both points
* @param {Vector} v
* @returns {Vector}
*/
centerPoint(v) {
const cx = this.x + v.x;
const cy = this.y + v.y;
return new Vector(cx / 2, cy / 2);
}
/**
* Computes componentwise floor and returns a new vector
* @returns {Vector}
*/
floor() {
return new Vector(Math.floor(this.x), Math.floor(this.y));
}
/**
* Computes componentwise ceil and returns a new vector
* @returns {Vector}
*/
ceil() {
return new Vector(Math.ceil(this.x), Math.ceil(this.y));
}
/**
* Computes componentwise round and return a new vector
* @returns {Vector}
*/
round() {
return new Vector(Math.round(this.x), Math.round(this.y));
}
/**
* Converts this vector from world to tile space and return a new vector
* @returns {Vector}
*/
toTileSpace() {
return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize));
}
/**
* Converts this vector from world to street space and return a new vector
* @returns {Vector}
*/
toStreetSpace() {
return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25));
}
/**
* Converts this vector to world space and return a new vector
* @returns {Vector}
*/
toWorldSpace() {
return new Vector(this.x * tileSize, this.y * tileSize);
}
/**
* Converts this vector to world space and return a new vector
* @returns {Vector}
*/
toWorldSpaceCenterOfTile() {
return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
}
/**
* Converts the top left tile position of this vector
* @returns {Vector}
*/
snapWorldToTile() {
return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize);
}
/**
* Normalizes the vector, dividing by the length(), and return a new vector
* @returns {Vector}
*/
normalize() {
const len = Math.max(1e-5, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len);
}
/**
* Normalizes the vector, dividing by the length(), and return a new vector
* @returns {Vector}
*/
normalizeIfGreaterOne() {
const len = Math.max(1, Math.hypot(this.x, this.y));
return new Vector(this.x / len, this.y / len);
}
/**
* Returns the normalized vector to the other point
* @param {Vector} v
* @returns {Vector}
*/
normalizedDirection(v) {
const dx = v.x - this.x;
const dy = v.y - this.y;
const len = Math.max(1e-5, Math.hypot(dx, dy));
return new Vector(dx / len, dy / len);
}
/**
* Returns a perpendicular vector
* @returns {Vector}
*/
findPerpendicular() {
return new Vector(-this.y, this.x);
}
/**
* Returns the unnormalized direction to the other point
* @param {Vector} v
* @returns {Vector}
*/
direction(v) {
return new Vector(v.x - this.x, v.y - this.y);
}
/**
* Returns a string representation of the vector
* @returns {string}
*/
toString() {
return this.x + "," + this.y;
}
/**
* Compares both vectors for exact equality. Does not do an epsilon compare
* @param {Vector} v
* @returns {Boolean}
*/
equals(v) {
return this.x === v.x && this.y === v.y;
}
/**
* Rotates this vector
* @param {number} angle
* @returns {Vector} new vector
*/
rotated(angle) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
}
/**
* Rotates this vector
* @param {number} angle
* @returns {Vector} this vector
*/
rotateInplaceFastMultipleOf90(angle) {
// const sin = Math.sin(angle);
// const cos = Math.cos(angle);
// let sin = 0, cos = 1;
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) {
case 0:
case 360: {
return this;
}
case 90: {
// sin = 1;
// cos = 0;
const x = this.x;
this.x = -this.y;
this.y = x;
return this;
}
case 180: {
// sin = 0
// cos = -1
this.x = -this.x;
this.y = -this.y;
return this;
}
case 270: {
// sin = -1
// cos = 0
const x = this.x;
this.x = this.y;
this.y = -x;
return this;
}
default: {
assertAlways(false, "Invalid fast inplace rotation: " + angle);
return this;
}
}
// return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
}
/**
* Rotates this vector
* @param {number} angle
* @returns {Vector} new vector
*/
rotateFastMultipleOf90(angle) {
assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);
switch (angle) {
case 360:
case 0: {
return new Vector(this.x, this.y);
}
case 90: {
return new Vector(-this.y, this.x);
}
case 180: {
return new Vector(-this.x, -this.y);
}
case 270: {
return new Vector(this.y, -this.x);
}
default: {
assertAlways(false, "Invalid fast inplace rotation: " + angle);
return new Vector();
}
}
}
/**
* Helper method to rotate a direction
* @param {enumDirection} direction
* @param {number} angle
* @returns {enumDirection}
*/
static transformDirectionFromMultipleOf90(direction, angle) {
if (angle === 0 || angle === 360) {
return direction;
}
assert(angle >= 0 && angle <= 360, "Invalid angle: " + angle);
switch (direction) {
case enumDirection.top: {
switch (angle) {
case 90:
return enumDirection.right;
case 180:
return enumDirection.bottom;
case 270:
return enumDirection.left;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.right: {
switch (angle) {
case 90:
return enumDirection.bottom;
case 180:
return enumDirection.left;
case 270:
return enumDirection.top;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.bottom: {
switch (angle) {
case 90:
return enumDirection.left;
case 180:
return enumDirection.top;
case 270:
return enumDirection.right;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
case enumDirection.left: {
switch (angle) {
case 90:
return enumDirection.top;
case 180:
return enumDirection.right;
case 270:
return enumDirection.bottom;
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
default:
assertAlways(false, "Invalid angle: " + angle);
return;
}
}
/**
* Compares both vectors for epsilon equality
* @param {Vector} v
* @returns {Boolean}
*/
equalsEpsilon(v, epsilon = 1e-5) {
return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
}
/**
* Returns the angle
* @returns {number} 0 .. 2 PI
*/
angle() {
return Math.atan2(this.y, this.x) + Math.PI / 2;
}
/**
* Serializes the vector to a string
* @returns {string}
*/
serializeTile() {
return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
}
/**
* Creates a simple representation of the vector
*/
serializeSimple() {
return { x: this.x, y: this.y };
}
/**
* @returns {number}
*/
serializeTileToInt() {
return this.x + this.y * 256;
}
/**
*
* @param {number} i
* @returns {Vector}
*/
static deserializeTileFromInt(i) {
const x = i % 256;
const y = Math.floor(i / 256);
return new Vector(x, y);
}
/**
* Deserializes a vector from a string
* @param {string} s
* @returns {Vector}
*/
static deserializeTile(s) {
return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
}
/**
* Deserializes a vector from a serialized json object
* @param {object} obj
* @returns {Vector}
*/
static fromSerializedObject(obj) {
if (obj) {
return new Vector(obj.x || 0, obj.y || 0);
}
}
}
/**
* Interpolates two vectors, for a = 0, returns v1 and for a = 1 return v2, otherwise interpolate
* @param {Vector} v1
* @param {Vector} v2
* @param {number} a
*/
export function mixVector(v1, v2, a) {
return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
}
/**
* Mapping from string direction to actual vector
* @enum {Vector}
*/
export const enumDirectionToVector = {
top: new Vector(0, -1),
right: new Vector(1, 0),
bottom: new Vector(0, 1),
left: new Vector(-1, 0),
};