UNPKG

@dominicstop/utils

Version:

Yet another event emitter written in typescript.

307 lines (246 loc) 6.6 kB
import { Cloneable } from "../types/Cloneable"; import { ValueRepresentable } from "../types/ValueRepresentable"; import { Angle } from "./Angle"; import { Point, PointValue } from "./Point"; export type Vector2DValue = { dx: number; dy: number; }; export class Vector2D implements Cloneable<Vector2D>, ValueRepresentable<Vector2DValue> { dx: number; dy: number; epsilon: number = 1e-10; constructor(args: Vector2DValue) { this.dx = args.dx; this.dy = args.dy; } get asValue(): Vector2DValue { return { dx: this.dx, dy: this.dy, }; } get asPoint(): Point { return new Point({ x: this.dx, y: this.dy, }); }; get asPointValue(): PointValue { return { x: this.dx, y: this.dy, }; }; // MARK: - Computed Properties // --------------------------- get magnitude(): number { return Math.sqrt(this.dx * this.dx + this.dy * this.dy); } get normalized(): Vector2D { const magnitude = this.magnitude; if (magnitude === 0) { return Vector2D.zero; }; return this.dividedByScalar(magnitude); } get isZero(): boolean { return ( Math.abs(this.dx) < this.epsilon && Math.abs(this.dy) < this.epsilon ); } get isUnit(): boolean { return Math.abs(this.magnitude - 1) < this.epsilon; } get perpendicular(): Vector2D { return new Vector2D({ dx: -this.dy, dy: this.dx }); } get inverse(): Vector2D { return new Vector2D({ dx: -this.dx, dy: -this.dy }); } /** * Returns the angle in radians from the positive x-axis to the vector. */ get angle(): Angle { const angle = Math.atan2(this.dy, this.dx); return Angle.initFromRadians(angle); }; // MARK: - Methods // --------------- clone(): Vector2D { return new Vector2D(this.asValue); }; computeDistanceFromOtherVector(otherVector: Vector2D): number { return Vector2D.distanceBetweenTwoVectors(this, otherVector); } isEqualToOtherVector( otherVector: Vector2D, tolerance: number = this.epsilon ): boolean { return ( Math.abs(this.dx - otherVector.dx) < tolerance && Math.abs(this.dy - otherVector.dy) < tolerance ); } // MARK: - Mutate Methods (Math Operations) // ------------------------------------- addWithOther(other: Vector2D): void { this.dx += other.dx; this.dy += other.dy; }; subtractFromOther(other: Vector2D): void { this.dx -= other.dx; this.dy -= other.dy; } multiplyByScalar(scalar: number): void { this.dx *= scalar; this.dy *= scalar; } divideByScalar(scalar: number): void { if (Math.abs(scalar) < this.epsilon) { this.dx = 0; this.dy = 0; } else { this.dx /= scalar; this.dy /= scalar; } } normalizeInPlace(): void { const mag = this.magnitude; if (mag >= this.epsilon) { this.divideByScalar(mag); } } // MARK: - Methods (Math Operations) // ----------------------------- addedWithOther(otherVector: Vector2D): Vector2D { return new Vector2D({ dx: this.dx + otherVector.dx, dy: this.dy + otherVector.dy, }); } subtractedWithOther(otherVector: Vector2D): Vector2D { return new Vector2D({ dx: this.dx - otherVector.dx, dy: this.dy - otherVector.dy, }); } multipliedByScalar(scalar: number): Vector2D { return new Vector2D({ dx: this.dx * scalar, dy: this.dy * scalar, }); } dividedByScalar(scalar: number): Vector2D { if (scalar === 0) { throw new Error("Cannot divide by zero."); } return new Vector2D({ dx: this.dx / scalar, dy: this.dy / scalar, }); } dotProductWithOtherVector(otherVector: Vector2D): number { return this.dx * otherVector.dx + this.dy * otherVector.dy; } /** * Rotates the vector by a given angle in radians. */ rotateByAngle(angle: Angle): Vector2D { const angleInRadians = angle.radians; const cos = Math.cos(angleInRadians); const sin = Math.sin(angleInRadians); return new Vector2D({ dx: this.dx * cos - this.dy * sin, dy: this.dx * sin + this.dy * cos, }); } projectOntoOtherVector(other: Vector2D): Vector2D { const scalar = this.dotProductWithOtherVector(other) / other.magnitude ** 2; return other.multipliedByScalar(scalar); } angleBetweenOtherVector(other: Vector2D): Angle { const dot = this.dotProductWithOtherVector(other); const mags = this.magnitude * other.magnitude; const angle = Math.acos(Math.min(Math.max(dot / mags, -1), 1)); return Angle.initFromRadians(angle); } crossProductWithOtherVector(other: Vector2D): number { return this.dx * other.dy - this.dy * other.dx; } reflectOverOtherVector(vector: Vector2D): Vector2D { const vectorNormalized = vector.normalized; const dot = this.dotProductWithOtherVector(vectorNormalized); return vectorNormalized .multipliedByScalar(2 * dot) .subtractedWithOther(this); } limit(maxMagnitude: number): Vector2D { return this.magnitude > maxMagnitude ? this.normalized.multipliedByScalar(maxMagnitude) : this.clone(); } // MARK: - Static Alias // -------------------- static get zero(): Vector2D { return new Vector2D({ dx: 0, dy: 0 }); } static get one(): Vector2D { return new Vector2D({ dx: 1, dy: 1 }); } static get unitX(): Vector2D { return new Vector2D({ dx: 1, dy: 0 }); } static get unitY(): Vector2D { return new Vector2D({ dx: 0, dy: 1 }); } // MARK: - Static Init // ------------------- static initFromAngle( angle: Angle, magnitude: number = 1 ): Vector2D { const radians = angle.radians; return new Vector2D({ dx: Math.cos(radians) * magnitude, dy: Math.sin(radians) * magnitude, }); } static initFromPoints( p1: PointValue, p2: PointValue ): Vector2D { return new Vector2D({ dx: p2.x - p1.x, dy: p2.y - p1.y, }); } // MARK: Static Methods // -------------------- static computeAverage(vectors: Vector2D[]): Vector2D { if (vectors.length === 0) return Vector2D.zero; const sum = vectors.reduce( (acc, v) => acc.addedWithOther(v), Vector2D.zero ); return sum.dividedByScalar(vectors.length); } static distanceBetweenTwoVectors( vectorA: Vector2D, vectorB: Vector2D ): number { const dx = vectorA.dx - vectorB.dx; const dy = vectorA.dy - vectorB.dy; return Math.sqrt(dx * dx + dy * dy); } }