type FixedPointSpec = { fractionalBits: number; totalBits: number; signed: boolean; }; export class FixedPoint { constructor( public readonly raw: bigint, public readonly fractionalBits: number, ) {} /** * Converts to a native number. * @note IEEE 754 doubles have 53 bits of precision. Large raw values (e.g. 64-bit * fixed) will lose some fractional precision; prefer `.raw` for calculations. */ toNumber(): number { const scale = 1n << BigInt(this.fractionalBits); return Number(this.raw) / Number(scale); } toString(): string { return this.toNumber().toString(); } toJSON(): number { return this.toNumber(); } static fromRaw(raw: bigint, fractionalBits: number): FixedPoint { return new FixedPoint(raw, fractionalBits); } static fromNumber( value: number, { fractionalBits, totalBits, signed }: FixedPointSpec, ): FixedPoint { if (!Number.isFinite(value)) { throw new RangeError('FixedPoint.fromNumber requires a finite number'); } const scale = 1n << BigInt(fractionalBits); // Use rounding to nearest. This uses JS numbers for the multiply which is // fine for UI-level inputs; for precise math prefer constructing from raw. const rawUnclamped = BigInt(Math.round(value * Number(scale))); const [min, max] = FixedPoint.bounds(totalBits, signed); const raw = FixedPoint.clamp(rawUnclamped, min, max); return new FixedPoint(raw, fractionalBits); } private static bounds(totalBits: number, signed: boolean): [bigint, bigint] { if (totalBits <= 0) { throw new RangeError('totalBits must be positive'); } if (signed) { const half = 1n << BigInt(totalBits - 1); return [-half, half - 1n]; } return [0n, (1n << BigInt(totalBits)) - 1n]; } private static clamp(val: bigint, min: bigint, max: bigint): bigint { if (val < min) return min; if (val > max) return max; return val; } }