Euclidean vector (also known as "Geometric" vector) library written in Typescript. A vector is an entity that has both magnitude and direction. Both 2D and 3D vectors are supported.
- Dependency-free;
- Extendable;
- Both immutable and mutable methods;
- Chainable API;
- Types included;
- Works in a browser and Node.js;
Package available via npm:
npm i @ericrovell/vector
import { vector } from "@ericrovell/vector";
vector(1, 2).toString(); // -> "(1, 2, 0)"Types for supported input are included into the package.
(x: number = 0, y: number = 0, z: number = 0)
Parses vector components from arguments.
vector().toString(); // -> "(0, 0, 0)"
vector(1).toString(); // -> "(1, 0, 0)"
vector(1, 2).toString(); // -> "(1, 2, 0)"
vector(1, 2, 3).toString(); // -> "(1, 2, 3)"
({ x: number = 0, y: number = 0, z: number = 0 }: Cartesian)
Parses the given input from Cartesian object and returns a new Vector instance.
/**
* Vector state defined in Cartesian coordinate system.
*/
interface Cartesian {
x?: number;
y?: number;
z?: number;
}
vector({ x: 1 }).toString(); // -> "(1, 0, 0)"
vector({ x: 1, y: 2 }).toString(); // -> "(1, 2, 0)"
vector({ x: 1, y: 2, z: 3 }).toString(); // -> "(1, 2, 3)"The Cartesian object is considered valid if it is contains at least one of coordinate components: x, y, or z. All missed components defaults to zero, extra data are simply ignored.
vector({ x: 1, data: "hello!" }).toString(); // -> "(1, 0, 0)"
vector({ x: 1, y: 2, z: 3, data: "hello!" }).toString(); // -> "(1, 2, 3)"
([ x: number, y: number = 0, z: number = 0 ]: CartesianTuple)
Parses the given input from CartesianTuple and returns a new Vector instance.
/**
* Tuple defining vector state defined in Cartesian coordinate system.
*/
type CartesianTuple = readonly [ x: number, y?: number, z?: number ];
vector([ 1 ]).toString(); // -> "(1, 0, 0)"
vector([ 1, 2 ]).toString(); // -> "(0, 2, 0)"
vector([ 1, 2, 3 ]).toString(); // -> "(0, 0, 3)"
({ degrees?: boolean, magnitude?: number = 1, phi: number = 0, theta: number = Math.PI / 2 }: Polar)
Parses the Polar input representing the vector in polar coordinates and returns a new Vector instance:
/**
* Vector state defined in Polar coordinate system:
*/
interface Polar {
degrees?: boolean = false;
magnitude?: number = 1;
phi: number;
theta?: number = Math.PI / 2;
}
vector({ phi: 0 }).toString() // -> "(1, 0, 0)"
vector({ phi: Math.PI / 2 })); // -> "(0, 1, 0)";
vector({
phi: Math.PI / 2,
theta: Math.PI / 2,
magnitude: 2
}) // -> "(0, 2, 0)";By default angles input require radians. To use degrees, pass a degrees boolean argument:
vector({ degrees: true, phi: 0 }) // -> "(1, 0, 0)");
vector({ degrees: true, phi: 90 }) // -> "(0, 1, 0)");
vector({ degrees: true, phi: 90, theta: 0, magnitude: 2 }) // -> "(0, 0, 2)");
vector({ degrees: true, phi: 90, theta: 90, magnitude: 2 }) // -> "(0, 2, 0)");The Polar object is considered valid if it is contains at least one of angle properties: phi or theta. The magnitude defaults to a unit length.
({ degrees?: boolean, p?: number = 1, phi: number = 0, z: number = 0 }: Cylindrical)
Parses the given input from Cylindrical representing the vector in cylindrical coordinate system and returns a new Vector instance:
/**
* Vector state defined in Cylindrical coordinate system:
*/
interface Cylindrical {
degrees?: boolean = false;
p: number = 1;
phi: number = 0;
z: number = 0;
}
vector({ p: Math.SQRT2, phi: Math.PI / 4, z: 5 })) // -> "(1, 1, 5)"
vector({ p: 7.0711, phi: -Math.PI / 4, z: 12 })) // -> "(5, -5, 12)"By default angles input require radians. To use degrees, pass a degrees boolean argument:
vector({ degrees: true, p: Math.SQRT2, phi: 45, z: 5 })) // -> "(1, 1, 5)"
vector({ degrees: true, p: 7.0711, phi: -45, z: 12 })) // -> "(5, -5, 12)"The Cylindrical object is considered valid if it is contains all the properties: p, phi, and z. Only degrees property is optional.
Most methods input arguments signature is:
(x: VectorInput | number, y?: number, z?: number)Where the VectorInput is any supported valid vector input representation. This way the valid input besides numeric arguments are:
Cartesian;CartesianTuple;Polar;Cylindrical;- another
Vectorinstance;
const instance = vector(1, 2, 3);
vector(1, 2, 3).add({ x: 1, y: 2, z: 3 }).toString(); // "(2, 4, 6)";
vector(1, 2, 3).add(instance).toString() // "(2, 4, 6)";
vector({ x: 1, y: 2, z: 3 }).add([ 1, 2, 3]).toString(); // "(2, 4, 6)";
.add(x: VectorInput | number, y?: number, z?: number): Vector
Performs the addition and returns the sum as new Vector instance.
vector(1, 2).add(3, 4).toString(); // -> "(4, 6, 0)"
.addSelf(x: VectorInput | number, y?: number, z?: number): Vector
Adds the another Vector instance or a valid vector input to this vector.
const v1 = vector(1, 2, 3).addSelf(1, 2, 3);
const v2 = vector(1, 2, 3);
v1.addSelf(v2);
v1.toString(); // -> "(2, 4, 6)"
.angle(input: VectorInput, signed = false, degrees = false): number
Calculates the angle between the vector instance and another valid vector input.
The angle can be signed if signed boolean argument is passed.
vector(1, 2, 3).angle(4, 5, 6) // -> 0.22573
vector(1, 2, 3).angle(4, 5, 6, true) // -> -0.22573
vector(1, 2, 3).angle(4, 5, 6, true, true) // -> -12.93315Note: this method do not accept simple arguments input.
.ceilSelf(places = 0): Vector
Rounds this vector's components values to the next upper bound with defined precision.
vector(1.12345, 2.45678, 3.78921).ceilSelf().toString() // -> "(2, 3, 4)");
vector(Math.SQRT2, Math.PI, 2 * Math.PI).ceilSelf(3).toString() // -> "(1.415, 3.142, 6.284)");
.clamp(min = 0, max = 1): Vector
Clamps this vector's component values between an upper and lower bound.
vector(1.2, -1).clamp().toString() // -> "(1, 0, 0)");
vector(5, 10, -2).clamp(2, 8).toString() // -> "(5, 8, 2)");
.copy(): Vector
Returns a copy of the vector instance.
const a = vector(1, 2, 3);
const b = a.copy();
b.toString(); // -> "(1, 2, 3)"
.cross(x: VectorInput | number, y?: number, z?: number): Vector
Calculates the cross product between the instance and another valid vector input and returns a new Vector instance.
vector(1, 2, 3).cross(4, 5, 6) // -> (-3, 6, -3)
.crossSelf(x: VectorInput | number, y?: number, z?: number): Vector
Sets this vector to the cross product between the original vector and another valid input.
vector(1, 2, 3).crossSelf(4, 5, 6) // -> (-3, 6, -3)
.distance(x: VectorInput | number, y?: number, z?: number): number
Calculates the Euclidean distance between the vector and another valid vector input, considering a point as a vector.
vector(1, 2, 3).distance(4, 5, 6) // -> 5.19615
.distanceSq(x: VectorInput | number, y?: number, z?: number): number
Calculates the squared Euclidean distance between the vector and another valid vector input, considering a point as a vector. Slightly more efficient to calculate, useful to comparing.
vector(1, 2, 3).distanceSq(4, 5, 6) // -> 27
.dot(x: VectorInput | number, y?: number, z?: number): number
Calculates the dot product of the vector and another valid vector input.
vector(1, 2, 3).dot(4, 5, 6) // -> 32
.equals(x: VectorInput | number, y?: number, z?: number): boolean
Performs an equality check against another valid vector input.
vector(1, 2, 3).equals(1, 2, 3); // -> true
vector({ x: 1, y: 2 }).equals([ 1, 2 ]); // -> true
vector({ x: -1, y: -2 }).equals({ x: -1, y: 2}); // -> false
.floorSelf(places = 0): Vector
Rounds this vector's components values to the next lower bound with defined precision.
vector(1.12345, 2.45678, 3.78921).floorSelf(4).toString() // -> "(1.1234, 2.4567, 3.7892)");
vector(Math.SQRT2, Math.PI, 2 * Math.PI).floorSelf(3).toString() // -> "(1.414, 3.141, 6.283)");
.getPhi(degrees = false): number
Calculates vector's azimuthal angle.
vector(3, 4).getPhi(); // -> 0.927295
vector(1, -2, 3).getPhi(true); // -> 53.130102
.getTheta(degrees = false): number
Calculates vector's elevation angle.
vector(3, 4, 5).getTheta(); // -> 0.785398
vector(3, 4, 5).getTheta(true); // -> 45
.inverted: Vector
Returns an inverted Vector instance.
vector(-1, 2).inverted; // -> "(1, -2, 0)"
.lerp(input: VectorInput, coef = 1): Vector
Linearly interpolate the vector to another vector.
const a = vector([ 4, 8, 16 ]);
const b = vector([ 8, 24, 48 ]);
a.lerp(b) // -> "(4, 8, 16)"
a.lerp(b, -0.5) // -> "(4, 8, 16)"
a.lerp(b, 0.25) // -> "(5, 12, 24)"
a.lerp(b, 0.5) // -> "(6, 16, 32)"
a.lerp(b, 0.75) // -> "(7, 20, 40)"
a.lerp(b, 1) // -> "(8, 24, 48)"
a.lerp(b, 1.5) // -> "(8, 24, 48)"Note: this method do not accept simple arguments input.
.limit(value: number): Vector
Limits the magnitude of the vector and returns the result as new Vector instance.
const v = vector(3, 4, 12); // magnitude is 13
v.limit(15).magnitude // -> 13
v.limit(10).magnitude // -> 10
v.limit(13).magnitude // -> 13
.limitSelf(value: number): Vector
Limits the magnitude of this vector and returns itself.
const v = vector(3, 4, 12); // magnitude is 13
v.limitSelf(15).magnitude // -> 13
v.limitSelf(10).magnitude // -> 10
v.limitSelf(13).magnitude // -> 13
.magnitude: number
Calculates the magnitude of the vector:
vector(0).magnitude; // -> 0
vector(3, 4).magnitude; // -> 5
vector(3, 4, 12).magnitude; // -> 13
.map(fn: (value: number) => number): Vector
Calls a defined callback on every vector component and returns a new Vector instance:
vector(1, 2, 3)
.map(value => value * 2)
.toString() // -> "(2, 4, 6)"
.mapSelf(fn: (value: number) => number): Vector
Calls a defined callback on each of this vector component.
const v = vector(1, 2, 3);
v.mapSelf(value => value * 2);
v.toString() // -> "(2, 4, 6)"
.magnitudeSq: number
Calculates the squared magnitude of the vector. It may be useful and faster where the real value is not that important. For example, to compare two vectors' length.
vector(0).magnitudeSq; // -> 0
vector(3, 4).magnitudeSq; // -> 25
vector(3, 4, 12).magnitudeSq; // -> 169
.normalize(): Vector
Normalizes the vector and returns a new Vector instance as unit vector:
vector().normalize().magnitude; // -> 1
vector(3, 4, 5).normalize().magnitude; // -> 1
.normalizeSelf(): Vector
Makes the current vector a unit vector.
vector().normalizeSelf().magnitude; // -> 0
vector(3, 4, 12).normalizeSelf().magnitude; // -> 13
.random2d(random = Math.random): Vector
Creates a random planar unit vector (OXY plane).
vector().random2d().toString() // -> "(0.23565, 0.75624, 0)"
.random3d(random = Math.random): Vector
Creates a random 3D unit vector.
Correct distribution thanks to wolfram.
vector().random3d().toString() // -> "(0.23565, 0.75624, -0.56571)"
.reflect(x: VectorInput | number, y?: number, z?: number): Vector
Reflects the vector about a normal line for 2D vector, or about a normal to a plane in 3D.
Here, in an example the vector a can be viewed as the incident ray, the vector n as the normal, and the resulting vector should be the reflected ray.
const a = vector([ 4, 6 ]);
const n = vector([ 0, -1 ]);
a.reflect(n).toString() // -> "(4, -6, 0)"
.rotate(value: number, degrees = false): Vector
Rotates the vector by an azimuthal angle (XOY plane) and returns a new Vector instance.
vector(1, 2).rotate(Math.PI / 3);
vector(1, 2).rotate(60, true);
.rotateSelf(value: number, degrees = false): Vector
Rotates the current vector by an azimuthal angle (XOY plane).
vector(1, 2).rotateSelf(Math.PI / 3);
vector(1, 2).rotateSelf(60, true);
.rotate3d(phi: number = 0, theta: number = 0, degrees = false): Vector
Rotates the vector by an azimuthal and elevation angles and returns a new Vector instance.
vector(1, 2, 3).rotate3d(Math.PI / 3, Math.PI / 6);
vector(1, 2, 3).rotate3d(60, 30, true);
.rotateSelf3d(phi: number = 0, theta: number = 0, degrees = false): Vector
Rotates the current vector by an azimuthal and elevation angles.
vector(1, 2, 3).rotateSelf3d(Math.PI / 3, Math.PI / 6);
vector(1, 2, 3).rotateSelf3d(60, 30, true);
.roundSelf(places = 0): Vector
Rounds this vector's component values to the closest bound with defined precision.
vector(1.12345, 2.45678, 3.78921).roundSelf(4).toString() // -> "(1.1235, 2.4568, 3.7892)");
vector(Math.SQRT2, Math.PI, 2 * Math.PI).roundSelf(3).toString() // -> "(1.414, 3.142, 6.283)");
.scale(value: number, inverse = false): Vector
Performs the scalar vector multiplication and returns a new Vector instance:
vector(1, 2).scale(2).toString(); // -> "(2, 4, 0)"
vector(1, 2, 3).scale(-2).toString(); // -> "(-2, -4, -6)"The second argument turns the passed value into reciprocal, in other words the division will be performed:
vector(2, 4, 6).scale(2, true).toString(); // -> "(1, 2, 3)"Although the same effect can be obtained just with .scale(0.5), it is useful when the variable may have zero value. In case of zero division the zero vector will be returned and marked as invalid.
const v = vector(1, 2, 3).scale(0, true);
v.valid // -> false
v.toString() // -> "(0, 0, 0)"
.scaleSelf(value: number, inverse = false): Vector
Scales this vector by a scalar value.
const a = vector(-1, 2, 3).scaleSelf(5);
a.toString() // -> "(-5, 10, 15)"The second parameter turns the passed value into reciprocal, in other words the division will be performed:
const v = vector(-12, -18, -24).scale(2, true);
v.toString(); // -> "(-6, -9, -12)"It is useful when the variable may have zero value. In this case the vector components won't change.
.setSelf(x: VectorInput | number, y?: number, z?: number): Vector
Set's the current vector state from another Vector instance or valid vector input.
const v1 = vector(1, 2, 3);
v1.setSelf(-1, -2, -3);
v1.toString() // -> "(-1, -2, -3)"
.setComponent(component: Component, value: number): Vector
Creates and returns a new Vector instance with modified component value.
vector(1, 2, 3).setComponent("x", 2).toString(); // -> "(2, 2, 3)"
vector(1, 2, 3).setComponent("y", 3).toString(); // -> "(1, 3, 3)"
vector(1, 2, 3).setComponent("z", 4).toString(); // -> "(1, 2, 4)"
.setComponentSelf(component: Component, value: number): Vector
Sets the vector instance component value.
const v = vector(1, 2, 3)
.setComponentSelf("x", 0)
.setComponentSelf("y", 0)
.setComponentSelf("z", 0)
v.toString() // -> "(0, 0, 0)"
.setMagnitude(value: number): Vector
Sets the magnitude of the vector and returns a new Vector instance.
vector(1).setMagnitude(5).magnitude // -> 5;
vector(1, 2, 3).setMagnitude(5).magnitude // -> 5;
.setMagnitudeSelf(value: number): Vector
Sets the magnitude of this vector.
vector(1).setMagnitudeSelf(5).magnitude // -> 5;
vector(1, 2, 3).setMagnitudeSelf(-5).magnitude // -> 5;
.setPhi(value: number, degrees = false): Vector
Rotates the vector instance to a specific azimuthal angle (OXY plane) and returns a new Vector instance.
vector(1, 2).setPhi(Math.PI / 3);
vector(1, 2, 3).setPhi(60, true);
.setPhiSelf(value: number, degrees = false): Vector
Rotates the vector instance to a specific azimuthal angle (OXY plane).
vector(1, 2).setPhiSelf(Math.PI / 3);
vector(1, 2, 3).setPhiSelf(60, true);
.setTheta(value: number, degrees = false): Vector
Rotates the vector instance to a specific elevation angle and returns a new Vector instance.
vector(1, 2).setTheta(Math.PI / 3);
vector(1, 2, 3).setTheta(60, true);
.setThetaSelf(value: number, degrees = false): Vector
Rotates the vector instance to a specific elevation angle.
vector(1, 2).setThetaSelf(Math.PI / 3);
vector(1, 2, 3).setThetaSelf(60, true);
.sub(x: VectorInput | number, y?: number, z?: number): Vector
Performs the subtraction and returns the result as new Vector instance.
vector(1, 2, 3).sub(2, 3, 4).toString() // -> "(-1, -1, -1)"
.subSelf(x: VectorInput | number, y?: number, z?: number): Vector
Subtracts another Vector instance or valid vector input from this vector.
const v1 = vector(1, 2, 3);
const v2 = vector(2, 1, 5);
v1.subSelf(v2);
v1.toString(); // -> "(-1, 1, -2)"
.toArray(): number[]
Returns vector's components packed into array.
vector(1).toArray(); // -> [ 1, 0, 0 ]
vector(1, 2).toArray(); // -> [ 1, 2, 0 ]
vector(1, 2, 3).toArray(); // -> [ 1, 2, 3 ]
.toString(): `(x: number, y: number, z: number)`
Returns a Vector string representation.
vector(1).toString(); // -> "(1, 0, 0)"
vector(1, 2).toString(); // -> "(1, 2, 0)"
vector(1, 2, 3).toString(); // -> "(1, 2, 3)"
.valid: boolean
Passing an invalid input does not throw error. Getter returns a boolean indicating whether user input was valid or not.
Invalid input defaults to zero vector.
vector([ 1, 2 ]).valid; // -> true
vector([ NaN ]).valid; // -> false
vector({ x: 1, y: 2 }).valid; // -> true
vector({ a: 1, b: 2 }).valid; // -> false
.valueOf(): number
Converts the vector instance to primitive value - it's magnitude. May be useful when using type coercion.
const a = vector(3, 4);
const b = vector(6, 8);
a + b // -> 15All operations have both mutable and immutable methods. They are easy to distinguish by self postfix:
.add()is immutable;addSelf()is mutable;
To extend the functionality for your needs, extend the parent Vector class:
import { Vector, type VectorInput } from "@ericrovell/vector";
class VectorExtended extends Vector {
constructor(input: VectorInput) {
super(input);
}
get sum() {
return this.x + this.y + this.z;
}
}
const instance = new VectorExtended([ 1, 2, 3 ]);
instance.sum; // -> 6Most of the methods are chainable, no matter is it mutable or immutable method:
const v = vector(1, 2, 3)
.add(1, 2, 3)
.sub(1, 2, 3)
.scale(2)
.toString(); // "(2, 4, 6)";
const v = vector(1, 2, 3)
.addSelf(1, 2, 3)
.subSelf(1, 2, 3)
.scaleSelf(2)
.toString(); // "(2, 4, 6)";The Vector instance can be iterated via for ... of loop to loop through the vector's components:
const v = vector(1, 2, 3);
for (const component of v) {
console.log(component);
// -> yielding 1, 2, 3
}The same way the spread operator can be used, Array.from(), and all other methods and functions that operates on iterables.
Tha package includes all necessary types useful for all possible valid input options are available for import:
export type {
Cartesian,
CartesianTuple,
Polar,
Cylindrical,
VectorInput,
Vector
} from "@ericrovell/vector";To run the tests use the npm run test command.
Vector's logo is done thanks to FreakAddL
