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
Vector
instance;
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.93315
Note: 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 // -> 15
All 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; // -> 6
Most 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