Skip to content

EricRovell/vector

Repository files navigation

Vector

V letter as a logo for Vector library

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.

Features

Getting started

Package available via npm:

npm i @ericrovell/vector
import { vector } from "@ericrovell/vector";

vector(1, 2).toString();  // -> "(1, 2, 0)"

Parsing

Input types

Types for supported input are included into the package.

Supported input

(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.

Methods input

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)";

API

.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

Other Features

Immutability

All operations have both mutable and immutable methods. They are easy to distinguish by self postfix:

  • .add() is immutable;
  • addSelf() is mutable;

Extendibility

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

Method Chaining

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)";

Iterability

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.

Types

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";

Tests

To run the tests use the npm run test command.

Attribution

Vector's logo is done thanks to FreakAddL