import { lerp } from "./utils.js";
/**
* Class representing a Vector.
*/
export class Vector {
/**
* Create a vector from coordinates.
* @param {number} [x=0] - x
* @param {number} [y=0] - y
* @param {number} [z=0] - z
*/
constructor(x = 0, y = 0, z = 0) {
if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") {
throw new TypeError("Vector components must be numbers.");
}
this.x = x;
this.y = y;
this.z = z;
}
/**
* Create a vector from an array.
* @param {number[]} array - Vector components as numbers in an array `[x, y, z]`.
* @returns {Vector}
*/
static fromArray(array) {
return new Vector(array[0], array[1], array[2] || 0);
}
/**
* Create an array from the vector's components.
* @returns {Array} an array `[x, y, z]`.
*/
toArray() {
return [this.x, this.y, this.z];
}
/**
* Create a 2D vector from an angle.
* @param {number} angle - The angle of the vector in radians.
* @param {number} [magnitude=1] - The magnitude of the vector.
* @returns {Vector}
*/
static fromAngle(angle, magnitude = 1) {
return new Vector(Math.cos(angle), Math.sin(angle)).multiply(magnitude);
}
/**
* Add a vector to this vector.
* @param {Vector} vector
* @returns {Vector}
*/
add(vector) {
this.x += vector.x;
this.y += vector.y;
this.z += vector.z;
return this;
}
/**
* Check if this vector is equivalent to another vector.
* @param {Vector} vector
* @returns {boolean}
*/
equals(vector) {
return this.x === vector.x && this.y === vector.y && this.z === vector.z;
}
/**
* Check if this vector is a point on a {@link Line}.
* @param {Line} line
* @returns {boolean}
*/
isOnLine(line) {
if (
(line.b.x - line.a.x) * (this.y - line.a.y) !==
(line.b.y - line.a.y) * (this.x - line.a.x)
) {
return false;
}
return (
Math.min(line.a.x, line.b.x) <= this.x &&
this.x <= Math.max(line.a.x, line.b.x) &&
Math.min(line.a.y, line.b.y) <= this.y &&
this.y <= Math.max(line.a.y, line.b.y)
);
}
/**
* Subtract a vector from this vector.
* @param {Vector} vector
* @returns {Vector}
*/
subtract(vector) {
this.x -= vector.x;
this.y -= vector.y;
this.z -= vector.z;
return this;
}
/**
* Multiply this vector by a scalar.
* @param {number} scalar
* @returns {Vector}
*/
multiply(scalar) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
return this;
}
/**
* Add two vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {Vector}
*/
static add(v1, v2) {
return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
}
/**
* Subtract two vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {Vector}
*/
static subtract(v1, v2) {
return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
}
/**
* Calculate the dot product of two vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {number}
*/
static dot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
/**
* Calculate the cross product of two 2D vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {number}
*/
static cross(v1, v2) {
return v1.x * v2.y - v1.y * v2.x;
}
/**
* Get the magnitude of this 2D vector.
* @returns {number} The magnitude (Euclidean distance) of this vector.
*/
getMagnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
/**
* Get the magnitude squared of this vector. This is faster than [getMagnitude]{@link Vector#getMagnitude} because it avoids using the square root.
* @returns {number} The magnitude squared of this vector.
*/
getMagnitudeSquared() {
return this.x * this.x + this.y * this.y;
}
/**
* Get the distance squared to another vector from this one.
* @param {Vector} vector - The target vector.
* @returns {number} The distance squared to the input vector.
*/
distanceSquared(vector) {
const delta = Vector.subtract(this, vector);
return delta.getMagnitudeSquared();
}
/**
* Set the magnitude of this vector.
* @param {number} magnitude
* @returns {Vector}
*/
setMagnitude(magnitude) {
return this.normalize().multiply(magnitude);
}
/**
* Calculate the distance to another vector from this vector
* @param {Vector} vector
* @returns {number}
*/
distance(vector) {
const delta = Vector.subtract(this, vector);
return delta.getMagnitude();
}
/**
* Calculate the distance between two vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {number}
*/
static distance(v1, v2) {
const delta = Vector.subtract(v1, v2);
return delta.getMagnitude();
}
/**
* Calculate the distance squared between two vectors.
* @param {Vector} v1
* @param {Vector} v2
* @returns {number}
*/
static distanceSquared(v1, v2) {
const delta = Vector.subtract(v1, v2);
return delta.getMagnitudeSquared();
}
/**
* Rotate this 2D vector by a specified angle.
* @param {number} angle - The angle to rotate the vector by, in radians.
* @returns {Vector} This vector after rotation.
*/
rotate(angle) {
const x = this.x * Math.cos(angle) - this.y * Math.sin(angle);
const y = this.x * Math.sin(angle) + this.y * Math.cos(angle);
this.x = x;
this.y = y;
return this;
}
/**
* Normalize this vector by setting it's magnitude to 1.
* @returns {Vector}
*/
normalize() {
const mag = this.getMagnitude();
if (mag === 0) {
throw new Error("Cannot normalize a zero vector.");
}
this.x /= mag;
this.y /= mag;
return this;
}
/**
* Make a copy of this vector.
* @returns {Vector}
*/
clone() {
return new Vector(this.x, this.y, this.z);
}
/**
* Get the 2D angle of this vector with respect to the positive x-axis.
* @returns {number} Angle in radians
*/
getAngle() {
return Math.atan2(this.y, this.x);
}
/**
* Linear interpolation between vectors.
* @param {Vector} v1
* @param {Vector} v2
* @param {number} amount
* @returns {Vector}
*/
static lerp(v1, v2, amount) {
return new Vector(lerp(v1.x, v2.x, amount), lerp(v1.y, v2.y, amount), lerp(v1.z, v2.z, amount));
}
/**
* Finds the `n` nearest neighbours to this vector, from a given array of vectors.
* @param {Array<Vector>} array - An array of Vectors to check.
* @param {number} n - The number of neighbours to find (must be > 0).
* @returns {Array<Vector>}
*/
nearestNeighbour(array, n) {
let neighbours = [];
for (let i = 0; i < n; i++) {
let record = Infinity;
let nearest = null;
for (let other of array) {
if (other != this && !neighbours.includes(other)) {
let dist = this.distance(other);
if (dist < record) {
nearest = other;
record = dist;
}
}
}
neighbours.push(nearest);
}
return neighbours;
}
}