Skip to content

Instantly share code, notes, and snippets.

@nandordudas
Last active August 29, 2024 05:01
Show Gist options
  • Save nandordudas/4c7c2b6e5ced07546a3862a170f69ed8 to your computer and use it in GitHub Desktop.
Save nandordudas/4c7c2b6e5ced07546a3862a170f69ed8 to your computer and use it in GitHub Desktop.
export class Vector2D implements Math.Vector2D {
static readonly #constructorSymbol = Symbol('Math.Vector2D')
static get zero(): Vector2D {
const zeroVector = Vector2D.create(0, 0)
return zeroVector
}
static create(
x: number,
y: number,
): Vector2D {
const newVector = new Vector2D(this.#constructorSymbol, x, y)
return newVector
}
/**
* Generates a random 2D vector.
*
* @param scale The scale of the random vector, can be a number or a vector. Defaults to 1.
* @param rng A random number generator function. Defaults to {@link Math.random}.
* @example
* Vector2D.random() // => Vector2D(0.123, 0.456)
* Vector2D.random(2) // => Vector2D(1.234, 0.987)
* Vector2D.random(Vector2D.create(2, 3)) // => Vector2D(1.234, 2.345)
* Vector2D.random(1, () => 0.5) // => Vector2D(0.5, 0.5)
* Vector2D.random(1, seededRandomNumberGenerator(12_345)) // => Vector2D(0.02040268573909998, 0.01654784823767841)
* Vector2D.random(1, seededRandomNumberGenerator()) // => pseudorandom numbers between 0 (inclusive) and 1 (exclusive)
*/
static random(scale?: number, rng?: () => number): Vector2D
static random<T extends Math.Vector2D>(vector: T, rng?: () => number): Vector2D
static random<T extends Math.Vector2D>(value: number | T = 1, rng: () => number = Math.random): Vector2D {
const randomVector = Vector2D.create(rng(), rng())
const scaledVector = randomVector.multiply<T>(value as T)
return scaledVector
}
static midpoint<T extends Math.Vector2D>(
v: T,
w: T,
): Vector2D {
const midpointVector = Vector2D.create((v.x + w.x) / 2, (v.y + w.y) / 2)
return midpointVector
}
#x: number
#y: number
#cachedMagnitude: number | null = null
get x(): number {
return this.#x
}
get y(): number {
return this.#y
}
constructor(
symbol: symbol,
x: number,
y: number,
) {
if (symbol !== Vector2D.#constructorSymbol)
raiseError('Vector2D is not constructable', TypeError)
assertIsNumber(x, 'Component x must be a number')
assertIsNumber(y, 'Component y must be a number')
this.#x = x
this.#y = y
}
clone(): Vector2D {
const clonedVector = Vector2D.create(this.#x, this.#y)
return clonedVector
}
isZero(): this is Vector2D & Math.ZeroCoordinates2D {
const isZeroVector = this.#x === 0 && this.#y === 0
return isZeroVector
}
isEqualTo<T extends Math.Vector2D>(vector: T): boolean {
const isVectorEqual = this.#x === vector.x && this.#y === vector.y
return isVectorEqual
}
add(scalar: number): this
add<T extends Math.Vector2D>(vector: T): this
add<T extends Math.Vector2D>(value: number | T): this {
if (isNumber(value)) {
this.#x += value
this.#y += value
}
else {
this.#x += value.x
this.#y += value.y
}
this.#cachedMagnitude = null
return this
}
subtract(scalar: number): this
subtract<T extends Math.Vector2D>(vector: T): this
subtract<T extends Math.Vector2D>(value: number | T): this {
if (isNumber(value)) {
this.#x -= value
this.#y -= value
}
else {
this.#x -= value.x
this.#y -= value.y
}
this.#cachedMagnitude = null
return this
}
multiply(scalar: number): this
multiply<T extends Math.Vector2D>(vector: T): this
multiply<T extends Math.Vector2D>(value: number | T): this {
if (isNumber(value)) {
this.#x *= value
this.#y *= value
}
else {
this.#x *= value.x
this.#y *= value.y
}
this.#cachedMagnitude = null
return this
}
divide(scalar: number): this
divide<T extends Math.Vector2D>(vector: T): this
divide<T extends Math.Vector2D>(value: number | T): this {
if (isNumber(value)) {
this.#x /= divideComponent(this.#x, value)
this.#y /= divideComponent(this.#y, value)
}
else {
this.#x /= divideComponent(this.#x, value.x)
this.#y /= divideComponent(this.#y, value.y)
}
this.#cachedMagnitude = null
return this
}
addImmutable(scalar: number): Vector2D
addImmutable<T extends Math.Vector2D>(vector: T): Vector2D
addImmutable<T extends Math.Vector2D>(value: number | T): Vector2D {
return this.clone().add<T>(value as T)
}
subtractImmutable(scalar: number): Vector2D
subtractImmutable<T extends Math.Vector2D>(vector: T): Vector2D
subtractImmutable<T extends Math.Vector2D>(value: number | T): Vector2D {
return this.clone().subtract<T>(value as T)
}
multiplyImmutable(scalar: number): Vector2D
multiplyImmutable<T extends Math.Vector2D>(vector: T): Vector2D
multiplyImmutable<T extends Math.Vector2D>(value: number | T): Vector2D {
return this.clone().multiply<T>(value as T)
}
divideImmutable(scalar: number): Vector2D
divideImmutable<T extends Math.Vector2D>(vector: T): Vector2D
divideImmutable<T extends Math.Vector2D>(value: number | T): Vector2D {
return this.clone().divide<T>(value as T)
}
magnitude(): number {
if (this.#cachedMagnitude !== null)
return this.#cachedMagnitude
if (this.isZero())
return 0
const magnitude = Math.hypot(this.#x, this.#y)
this.#cachedMagnitude = magnitude
return magnitude
}
normalize(): this {
const magnitude = this.magnitude()
if (magnitude === 0)
return this
const normalizedVector = this.divide(magnitude)
return normalizedVector
}
dotProduct<T extends Math.Vector2D>(vector: T): number {
if (this.isZero() || vector.isZero())
return 0
const dotProductResult = this.#x * vector.x + this.#y * vector.y
return dotProductResult
}
crossProduct<T extends Math.Vector2D>(vector: T): number {
if (this.isZero() || vector.isZero())
return 0
const crossProductResult = this.#x * vector.y - this.#y * vector.x
return crossProductResult
}
distanceTo<T extends Math.Vector2D>(vector: T): number {
const distance = Math.hypot(this.#x - vector.x, this.#y - vector.y)
return distance
}
toString(): string {
const stringRepresentation = `Vector2D(${this.x}, ${this.y})`
return stringRepresentation
}
*[Symbol.iterator](): Generator<number> {
yield this.#x
yield this.#y
}
}
export function vector(x = 0, y = 0): Vector2D {
const newVector = Vector2D.create(x, y)
return newVector
}
export function divideComponent(
component: number,
divisor: number,
): number {
assertIsNumber(component, 'Component must be a number')
assertIsNumber(divisor, 'Divisor must be a number')
if (Math.abs(divisor) < Number.EPSILON) {
console.warn('Division by near-zero value. Returning maximum safe value.')
const divisionResult = component < 0 ? -Number.MAX_VALUE : Number.MAX_VALUE
return divisionResult
}
const divisionResult = component / divisor
return divisionResult
}
export function assertIsNumber(
value: unknown,
message: string,
): asserts value is number {
assert(isNumber(value), message, TypeError)
}
export function assert(
condition: boolean,
message: string,
ErrorType: Utils.ErrorBuilder = Error,
): asserts condition {
if (!condition)
raiseError(message, ErrorType)
}
export function raiseError(
message: string,
ErrorType: Utils.ErrorBuilder = Error,
): never {
throw new ErrorType(message)
}
export function isNumber(value: unknown): value is number {
const isValueNumber = typeof value === 'number'
return isValueNumber
}
/**
* Creates a seeded pseudorandom number generator (PRNG) using the
* Linear Congruential Generator (LCG) algorithm.
*
* @param seed Optional seed value for the PRNG. If not provided, a
* timestamp-based seed is used for better randomness.
* @returns A function that generates pseudorandom numbers between 0 (inclusive)
* and 1 (exclusive).
* @example
* const randomGenerator = seededRandomNumberGenerator()
* randomGenerator() // => pseudorandom numbers between 0 (inclusive) and 1 (exclusive)
* Array.from({ length: 2 }, seededRandomNumberGenerator(12_345)) // [0.02040268573909998, 0.01654784823767841]
*/
export function seededRandomNumberGenerator(seed?: number): () => number {
let state = seed ?? performance.now()
const multiplier = 1_664_525
const increment = 1_013_904_223
const modulus = 4_294_967_296
return () => {
state = (state * multiplier + increment) & (modulus - 1)
const randomFraction = state / modulus
return randomFraction
}
}
declare namespace Utils {
type Union<T extends any[]> = T extends [infer First, ...infer Rest] ? (First & Union<Rest>) : unknown
type ErrorBuilder = new (message?: string) => Error
}
declare namespace Math {
type OverloadedMethod<T extends Vector2D> = Utils.Union<[
(scalar: number) => T,
<K extends T>(vector: K) => T,
]>
interface Coordinates2D { x: number, y: number }
interface ZeroCoordinates2D extends Coordinates2D { x: 0, y: 0 }
interface Vector2D {
get x(): number
get y(): number
clone: () => Vector2D
isEqualTo: <T extends Vector2D>(vector: T) => boolean
isZero: () => this is Vector2D & ZeroCoordinates2D
add: OverloadedMethod<this>
subtract: OverloadedMethod<this>
multiply: OverloadedMethod<this>
divide: OverloadedMethod<this>
addImmutable: OverloadedMethod<Vector2D>
subtractImmutable: OverloadedMethod<Vector2D>
multiplyImmutable: OverloadedMethod<Vector2D>
divideImmutable: OverloadedMethod<Vector2D>
magnitude: () => number
normalize: () => this
dotProduct: <T extends Vector2D>(vector: T) => number
crossProduct: <T extends Vector2D>(vector: T) => number
distanceTo: <T extends Vector2D>(vector: T) => number
[Symbol.iterator]: () => Generator<number>
}
}
@nandordudas
Copy link
Author

@nandordudas
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment