Last active
August 29, 2024 05:01
-
-
Save nandordudas/4c7c2b6e5ced07546a3862a170f69ed8 to your computer and use it in GitHub Desktop.
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
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> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Predictable Chaos