Skip to content

Instantly share code, notes, and snippets.

@nandordudas
Last active August 9, 2024 13:13
Show Gist options
  • Save nandordudas/73efbde1a69e24c9125b1085f05b1d68 to your computer and use it in GitHub Desktop.
Save nandordudas/73efbde1a69e24c9125b1085f05b1d68 to your computer and use it in GitHub Desktop.
Vector 2D
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