Last active
August 9, 2024 13:13
-
-
Save nandordudas/73efbde1a69e24c9125b1085f05b1d68 to your computer and use it in GitHub Desktop.
Vector 2D
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Constructor<T> = new (...args: any[]) => T | |
type Intersection<T extends any[]> = T extends [infer First, ...infer Rest] | |
? First & Intersection<Rest> | |
: unknown | |
type Brand<T, B> = T & { __brand: B } | |
type Scalar = Brand<number, 'scalar'> | |
type Radians = Brand<number, 'radians'> | |
type Array2DContract = [number, number] | |
interface Vector2DCoordinatesContract { | |
x: number | |
y: number | |
} | |
type ScalarOrVector2D = Scalar | Vector2DCoordinatesContract | |
type Operation<T, R extends Vector2DBasicContract> = (input: T) => R | |
type ScalarAndVectorOperation = Intersection<[ | |
Operation<Scalar, Vector2DBasicContract>, | |
Operation<Vector2DCoordinatesContract, Vector2DBasicContract>, | |
]> | |
type Vector2DContract = Intersection<[ | |
Vector2DBasicContract, | |
Vector2DUtilityContract, | |
Vector2DAlgebraContract, | |
Vector2DTrigonometryContract, | |
Vector2DTransformationContract, | |
]> | |
type Vector2DCoordinateWithUtilityContract = Intersection<[ | |
Vector2DCoordinatesContract, | |
Vector2DUtilityContract, | |
]> | |
interface Vector2DBasicContract extends Readonly<Vector2DCoordinatesContract> { | |
add: ScalarAndVectorOperation | |
subtract: ScalarAndVectorOperation | |
multiply: ScalarAndVectorOperation | |
divide: ScalarAndVectorOperation | |
isEquals: (other: Vector2DContract) => boolean | |
isZero: () => boolean | |
isOnAxis: (axis?: keyof Vector2DCoordinatesContract) => boolean | |
} | |
interface Vector2DAlgebraContract { | |
dot: (other: Vector2DContract) => Scalar | |
cross: (other: Vector2DContract) => Scalar | |
magnitudeSquared: () => Scalar | |
magnitude: () => Scalar | |
normalize: () => Vector2DBasicContract | |
} | |
interface Vector2DTrigonometryContract { | |
angle: () => Scalar | |
rotate: (angle: Radians) => Vector2DContract | |
distanceTo: (other: Vector2DContract) => Scalar | |
} | |
interface Vector2DTransformationContract { | |
reflect: (normal: Vector2DContract) => Vector2DContract | |
limit: <T extends Vector2DContract>(min: Scalar, max: Scalar) => T | |
lerp: (other: Vector2DContract, t: Scalar) => Vector2DContract | |
} | |
interface Vector2DUtilityContract { | |
isInstanceOf: <T extends Vector2DContract>(type: Constructor<T>) => boolean | |
toString: () => string | |
toArray: () => Array2DContract | |
toObject: () => Vector2DCoordinatesContract | |
clone: () => Vector2DBasicContract | |
} | |
class Vector2DBase implements Vector2DCoordinateWithUtilityContract { | |
static readonly zero: Vector2DBase = new Vector2DBase(0, 0) | |
static readonly one: Vector2DBase = new Vector2DBase(1, 1) | |
static fromArray(...array: Array2DContract): Vector2DBase { | |
return new Vector2DBase(...array) | |
} | |
static fromObject({ x, y }: Vector2DCoordinatesContract): Vector2DBase { | |
return new Vector2DBase(x, y) | |
} | |
static fromAngle(angle: Radians, length = 1 as Scalar): Vector2DBase { | |
const cos = Math.cos(angle) | |
const sin = Math.sin(angle) | |
return new Vector2DBase(cos * length, sin * length) | |
} | |
constructor( | |
public readonly x: number, | |
public readonly y: number, | |
) { Object.freeze(this) } | |
isInstanceOf<T extends Vector2DContract>(type: Constructor<T>): boolean { | |
return this instanceof type | |
} | |
toString(): string { | |
return `${this.constructor.name}(${this.x}, ${this.y})` | |
} | |
toArray(): Array2DContract { | |
return [this.x, this.y] | |
} | |
toObject(): Vector2DCoordinatesContract { | |
return Object.freeze<Vector2DCoordinatesContract>( | |
Object.create(null, { | |
x: { value: this.x }, | |
y: { value: this.y }, | |
}), | |
) | |
} | |
clone(): Vector2DBasicContract { | |
return new Vector2DBasic(this.x, this.y) | |
} | |
} | |
class Vector2DBasic extends Vector2DBase implements Vector2DBasicContract { | |
constructor( | |
public readonly x: number, | |
public readonly y: number, | |
) { super(x, y) } | |
add<T extends Vector2DBasicContract>(scalarOrVector2D: ScalarOrVector2D): T { | |
const other = extractVector2DBasic(scalarOrVector2D) | |
return new (this.constructor as Constructor<T>)(this.x + other.x, this.y + other.y) | |
} | |
subtract<T extends Vector2DBasicContract>(scalarOrVector2D: ScalarOrVector2D): T { | |
const other = extractVector2DBasic(scalarOrVector2D) | |
return new (this.constructor as Constructor<T>)(this.x - other.x, this.y - other.y) | |
} | |
multiply<T extends Vector2DBasicContract>(scalarOrVector2D: ScalarOrVector2D): T { | |
const other = extractVector2DBasic(scalarOrVector2D) | |
return new (this.constructor as Constructor<T>)(this.x * other.x, this.y * other.y) | |
} | |
divide<T extends Vector2DBasicContract>(scalarOrVector2D: ScalarOrVector2D): T { | |
const other = extractVector2DBasic(scalarOrVector2D) | |
if (other.isOnAxis()) | |
throw new TypeError('Division by a collinear vector is not allowed') | |
return new (this.constructor as Constructor<T>)(this.x / other.x, this.y / other.y) | |
} | |
isEquals<T extends Vector2DBasicContract>(other: T): boolean { | |
return this.x === other.x && this.y === other.y | |
} | |
isZero(): boolean { | |
return this.x === 0 && this.y === 0 | |
} | |
isOnAxis(axis?: keyof Vector2DCoordinatesContract): boolean { | |
if (!axis) | |
return this.x === 0 || this.y === 0 | |
return this[axis] === 0 | |
} | |
} | |
function extractVector2DBasic(scalarOrVector2D: ScalarOrVector2D): Vector2DBasicContract { | |
if (isScalar(scalarOrVector2D)) | |
return new Vector2DBasic(scalarOrVector2D, scalarOrVector2D) | |
return new Vector2DBasic(scalarOrVector2D.x, scalarOrVector2D.y) | |
} | |
function isScalar(value: unknown): value is Scalar { | |
return typeof value === 'number' && !Number.isNaN(value) | |
} | |
class Vector2DAlgebraic extends Vector2DBasic implements Vector2DAlgebraContract { | |
dot<T extends Vector2DBasicContract>(other: T): Scalar { | |
return this.x * other.x + this.y * other.y as Scalar | |
} | |
cross<T extends Vector2DBasicContract>(other: T): Scalar { | |
return this.x * other.y - this.y * other.x as Scalar | |
} | |
magnitudeSquared(): Scalar { | |
return this.dot(this) | |
} | |
magnitude(): Scalar { | |
return Math.hypot(this.x, this.y) as Scalar | |
} | |
normalize<T extends Vector2DBasicContract>(): T { | |
return this.divide(this.magnitude()) | |
} | |
} | |
class Vector2DTrigonometric extends Vector2DAlgebraic { | |
angle(): Scalar { | |
return Math.atan2(this.y, this.x) as Scalar | |
} | |
rotate(angle: Radians): Vector2DBasicContract { | |
const cos = Math.cos(angle) | |
const sin = Math.sin(angle) | |
return new Vector2DBasic(cos * this.x - sin * this.y, sin * this.x + cos * this.y) | |
} | |
distanceTo<T extends Vector2DBasicContract>(other: T): Scalar { | |
const difference = this.subtract<Vector2DContract>(other) | |
return Math.hypot(difference.x, difference.y) as Scalar | |
} | |
} | |
class Vector2DTransformation extends Vector2DTrigonometric implements Vector2DTransformationContract { | |
limit<T extends Vector2DContract>(min: Scalar, max: Scalar): T { | |
const magnitude = this.magnitude() | |
if (magnitude === 0) | |
throw new TypeError('A vector with zero magnitude cannot be limited') | |
if (magnitude < min) | |
return this.multiply(min / magnitude as Scalar) | |
if (magnitude > max) | |
return this.multiply(max / magnitude as Scalar) | |
return new (this.constructor as Constructor<T>)(this.x, this.y) | |
} | |
lerp<T extends Vector2DContract>(other: T, t: Scalar): Vector2DContract { | |
const difference = other.subtract(this) | |
return this.add(difference.multiply(t)) | |
} | |
reflect<T extends Vector2DBasicContract>(normal: T): Vector2DContract { | |
const dot = this.dot(normal) | |
const reflection = normal.multiply(2.0 * dot as Scalar) | |
return this.subtract(reflection) | |
} | |
} | |
export class Vector2D extends Vector2DTransformation { } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment