Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fe1be27
feat(core): accept readonly vectors in vec2 operations
RodrigoHamuy Sep 9, 2026
96d560f
feat(core): accept readonly vectors in vec3 operations
RodrigoHamuy Sep 10, 2026
a7b2a0b
feat(core): accept readonly vectors in vec4 operations
RodrigoHamuy Sep 10, 2026
44ca6dd
feat(core): accept readonly quaternions in quat operations
RodrigoHamuy Sep 10, 2026
7b74b6f
feat(core): accept readonly dual quaternions in quat2 operations
RodrigoHamuy Sep 10, 2026
b60d340
feat(core): accept readonly matrices in mat2 operations
RodrigoHamuy Sep 10, 2026
937f473
feat(core): accept readonly matrices in mat2d operations
RodrigoHamuy Sep 10, 2026
5e2b2b1
feat(core): accept readonly matrices in mat3 operations
RodrigoHamuy Sep 10, 2026
96fe4e0
feat(core): accept readonly matrices in mat4 operations
RodrigoHamuy Sep 10, 2026
1cf8d5e
feat(core): accept readonly euler angles in euler operations
RodrigoHamuy Sep 10, 2026
a2371f1
feat(core): accept readonly coordinates in spherical operations
RodrigoHamuy Sep 10, 2026
1f7a4f3
feat(core): accept readonly coordinates in polar operations
RodrigoHamuy Sep 10, 2026
8eaf636
feat(color): accept readonly colors in color operations
RodrigoHamuy Sep 10, 2026
e07409b
feat(color): accept readonly colors in hsl operations
RodrigoHamuy Sep 10, 2026
82ff443
feat(shapes): accept readonly boxes in box2 operations
RodrigoHamuy Sep 10, 2026
b5b3f4b
feat(shapes): accept readonly boxes in box3 operations
RodrigoHamuy Sep 10, 2026
60928eb
feat(shapes): accept readonly spheres in sphere operations
RodrigoHamuy Sep 10, 2026
8e8c524
feat(shapes): accept readonly circles in circle operations
RodrigoHamuy Sep 10, 2026
4e93789
feat(shapes): accept readonly planes in plane3 operations
RodrigoHamuy Sep 10, 2026
7ee3270
feat(shapes): accept readonly boxes in obb3 operations
RodrigoHamuy Sep 10, 2026
c06d94f
feat(shapes): accept readonly frustums in frustum operations
RodrigoHamuy Sep 10, 2026
b078a1c
feat(shapes): accept readonly vectors in segment2 operations
RodrigoHamuy Sep 10, 2026
c410b78
feat(shapes): accept readonly vectors in triangle2 operations
RodrigoHamuy Sep 10, 2026
3afcc03
feat(shapes): accept readonly vectors in triangle3 operations
RodrigoHamuy Sep 10, 2026
d5c982e
feat(shapes): accept readonly inputs in raycast3 operations
RodrigoHamuy Sep 10, 2026
b991438
feat(shapes): accept readonly vertices in polygon2 operations
RodrigoHamuy Sep 10, 2026
3fa3f4a
feat(geometry): accept readonly inputs in geometry operations
RodrigoHamuy Sep 10, 2026
14b220d
feat(time): accept readonly vectors in spring operations
RodrigoHamuy Sep 10, 2026
19f419a
feat(ik): accept readonly vectors in fabrik operations
RodrigoHamuy Sep 10, 2026
38a4ec5
feat(random): accept a readonly array in choice
RodrigoHamuy Sep 10, 2026
c76e713
feat(core): use readonly aliases for foreign types across modules
RodrigoHamuy Sep 10, 2026
13ebf98
chore: add changeset for readonly types
RodrigoHamuy Sep 15, 2026
217bd6c
add DeepReadonly
RodrigoHamuy Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-otters-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"math": minor
---

accepts readonly inputs across core, shapes, color, geometry, time, ik and random operations
37 changes: 20 additions & 17 deletions src/color/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export * from './parse';
/** A linear-sRGB color: [r, g, b] floats in [0, 1]. */
export type Color = [r: number, g: number, b: number];

/** A read-only linear sRGB color */
export type RColor = Readonly<Color>;

/** Accepted input types for creating or parsing a Color. */
export type ColorInput =
| string // '#f00', '#ff0000', 'red', 'rgb(255,0,0)', 'hsl(0,100%,50%)'
Expand All @@ -22,12 +25,12 @@ export function fromValues(r: number, g: number, b: number): Color {
}

/** Create a new Color that is a copy of `c`. */
export function clone(c: Color): Color {
export function clone(c: RColor): Color {
return [c[0], c[1], c[2]];
}

/** Copy the values from `src` into `out`. Returns `out`. */
export function copy(out: Color, src: Color): Color {
export function copy(out: Color, src: RColor): Color {
out[0] = src[0];
out[1] = src[1];
out[2] = src[2];
Expand All @@ -54,104 +57,104 @@ export function setScalar(out: Color, s: number): Color {
* Set `out` from an sRGB gamma-encoded [r, g, b] array with values in [0, 1].
* Converts from sRGB gamma space to linear. Returns `out`.
*/
export function setFromSRGB(out: Color, srgb: [number, number, number]): Color {
export function setFromSRGB(out: Color, srgb: readonly [number, number, number]): Color {
out[0] = srgbToLinear(srgb[0]);
out[1] = srgbToLinear(srgb[1]);
out[2] = srgbToLinear(srgb[2]);
return out;
}

/** Create a new Color from an sRGB gamma-encoded [r, g, b] array with values in [0, 1]. */
export function fromSRGB(srgb: [number, number, number]): Color {
export function fromSRGB(srgb: readonly [number, number, number]): Color {
return setFromSRGB(create(), srgb);
}

/** Write the sRGB gamma-encoded [r, g, b] of a linear Color into `out` (values [0, 1]). */
export function toSRGB(out: [number, number, number], c: Color): [number, number, number] {
export function toSRGB(out: [number, number, number], c: RColor): [number, number, number] {
out[0] = linearToSrgb(c[0]);
out[1] = linearToSrgb(c[1]);
out[2] = linearToSrgb(c[2]);
return out;
}

/** Create a CSS `rgb(...)` string in sRGB gamma space (for HTML/canvas use). */
export function toCSS(c: Color): string {
export function toCSS(c: RColor): string {
return `rgb(${to255(c[0])}, ${to255(c[1])}, ${to255(c[2])})`;
}

/** Convert to a 0xRRGGBB integer in sRGB gamma space. */
export function toHex(c: Color): number {
export function toHex(c: RColor): number {
return (to255(c[0]) << 16) | (to255(c[1]) << 8) | to255(c[2]);
}

/** Convert to a 6-digit sRGB hex string without a leading '#', e.g. 'ff8800'. */
export function toHexString(c: Color): string {
export function toHexString(c: RColor): string {
return toHex(c).toString(16).padStart(6, '0');
}

/** Add `a + b` component-wise into `out`. Returns `out`. */
export function add(out: Color, a: Color, b: Color): Color {
export function add(out: Color, a: RColor, b: RColor): Color {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
out[2] = a[2] + b[2];
return out;
}

/** Add scalar `s` to each channel of `a` into `out`. Returns `out`. */
export function addScalar(out: Color, a: Color, s: number): Color {
export function addScalar(out: Color, a: RColor, s: number): Color {
out[0] = a[0] + s;
out[1] = a[1] + s;
out[2] = a[2] + s;
return out;
}

/** Subtract `a - b` component-wise into `out`. Returns `out`. */
export function sub(out: Color, a: Color, b: Color): Color {
export function sub(out: Color, a: RColor, b: RColor): Color {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
out[2] = a[2] - b[2];
return out;
}

/** Multiply `a * b` component-wise into `out` (tinting). Returns `out`. */
export function multiply(out: Color, a: Color, b: Color): Color {
export function multiply(out: Color, a: RColor, b: RColor): Color {
out[0] = a[0] * b[0];
out[1] = a[1] * b[1];
out[2] = a[2] * b[2];
return out;
}

/** Scale each channel of `a` by `s` into `out` (brightness). Returns `out`. */
export function multiplyScalar(out: Color, a: Color, s: number): Color {
export function multiplyScalar(out: Color, a: RColor, s: number): Color {
out[0] = a[0] * s;
out[1] = a[1] * s;
out[2] = a[2] * s;
return out;
}

/** Linearly interpolate from `a` to `b` by `t` into `out` (physically-correct blend). Returns `out`. */
export function lerp(out: Color, a: Color, b: Color, t: number): Color {
export function lerp(out: Color, a: RColor, b: RColor, t: number): Color {
out[0] = a[0] + (b[0] - a[0]) * t;
out[1] = a[1] + (b[1] - a[1]) * t;
out[2] = a[2] + (b[2] - a[2]) * t;
return out;
}

/** Clamp each channel of `c` to [0, 1] into `out`. Returns `out`. */
export function clamp(out: Color, c: Color): Color {
export function clamp(out: Color, c: RColor): Color {
out[0] = clamp01(c[0]);
out[1] = clamp01(c[1]);
out[2] = clamp01(c[2]);
return out;
}

/** Whether `a` and `b` are equal, within an optional per-channel `epsilon` (default exact). */
export function equals(a: Color, b: Color, epsilon = 0): boolean {
export function equals(a: RColor, b: RColor, epsilon = 0): boolean {
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon && Math.abs(a[2] - b[2]) <= epsilon;
}

/** Relative luminance in [0, 1] (Rec. 709 weights, on linear light). */
export function luminance(c: Color): number {
export function luminance(c: RColor): number {
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}

Expand Down
6 changes: 3 additions & 3 deletions src/color/colorspace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Color } from './color';
import type { Color, RColor } from './color';

// Color-space conversions (pure functions — no global working-space state).
//
Expand All @@ -21,7 +21,7 @@ export function linearToSrgb(c: number): number {
* Convert a linear-sRGB Color to linear Display-P3 primaries, into `out`. Returns `out`.
* (Both spaces share the sRGB transfer curve; this changes only the primaries.)
*/
export function linearSrgbToLinearDisplayP3(out: Color, c: Color): Color {
export function linearSrgbToLinearDisplayP3(out: Color, c: RColor): Color {
const r = c[0];
const g = c[1];
const b = c[2];
Expand All @@ -35,7 +35,7 @@ export function linearSrgbToLinearDisplayP3(out: Color, c: Color): Color {
* Convert a linear Display-P3 Color to linear-sRGB primaries, into `out`. Returns `out`.
* Colors outside the sRGB gamut yield channels outside [0, 1] — clamp if needed.
*/
export function linearDisplayP3ToLinearSrgb(out: Color, c: Color): Color {
export function linearDisplayP3ToLinearSrgb(out: Color, c: RColor): Color {
const r = c[0];
const g = c[1];
const b = c[2];
Expand Down
17 changes: 10 additions & 7 deletions src/color/hsl.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Color } from './color';
import type { Color, RColor } from './color';
import { linearToSrgb, srgbToLinear } from './colorspace';

/** A hue-saturation-lightness color: [h, s, l], all in [0, 1] (hue wraps). */
export type HSL = [hue: number, saturation: number, lightness: number];

/** A read-only HSL color */
export type RHSL = Readonly<HSL>;

/** Create a new HSL initialized to [0, 0, 0] (black). */
export function create(): HSL {
return [0, 0, 0];
Expand All @@ -15,12 +18,12 @@ export function fromValues(h: number, s: number, l: number): HSL {
}

/** Create a new HSL that is a copy of `a`. */
export function clone(a: HSL): HSL {
export function clone(a: RHSL): HSL {
return [a[0], a[1], a[2]];
}

/** Copy the values from `src` into `out`. Returns `out`. */
export function copy(out: HSL, src: HSL): HSL {
export function copy(out: HSL, src: RHSL): HSL {
out[0] = src[0];
out[1] = src[1];
out[2] = src[2];
Expand All @@ -36,7 +39,7 @@ export function set(out: HSL, h: number, s: number, l: number): HSL {
}

/** Write the HSL of a linear Color into `out`. Returns `out`. */
export function fromColor(out: HSL, c: Color): HSL {
export function fromColor(out: HSL, c: RColor): HSL {
// linear -> sRGB gamma; HSL is defined on gamma-encoded sRGB
const r = linearToSrgb(c[0]);
const g = linearToSrgb(c[1]);
Expand Down Expand Up @@ -64,7 +67,7 @@ export function fromColor(out: HSL, c: Color): HSL {
}

/** Write the linear Color of an HSL into `out`. Returns `out`. */
export function toColor(out: Color, a: HSL): Color {
export function toColor(out: Color, a: RHSL): Color {
const h = a[0];
const s = a[1];
const l = a[2];
Expand All @@ -90,7 +93,7 @@ export function toColor(out: Color, a: HSL): Color {
* the hue wheel (so e.g. 350°→10° passes through 0°, not all the way back).
* Returns `out`.
*/
export function lerp(out: HSL, a: HSL, b: HSL, t: number): HSL {
export function lerp(out: HSL, a: RHSL, b: RHSL, t: number): HSL {
let dh = b[0] - a[0];
if (dh > 0.5) dh -= 1;
else if (dh < -0.5) dh += 1;
Expand All @@ -108,7 +111,7 @@ export function lerp(out: HSL, a: HSL, b: HSL, t: number): HSL {
* Offset `a` by (dh, ds, dl) into `out`: hue wraps into [0, 1), saturation and
* lightness are clamped to [0, 1]. Returns `out`.
*/
export function offset(out: HSL, a: HSL, dh: number, ds: number, dl: number): HSL {
export function offset(out: HSL, a: RHSL, dh: number, ds: number, dl: number): HSL {
let h = a[0] + dh;
h -= Math.floor(h);
out[0] = h;
Expand Down
4 changes: 2 additions & 2 deletions src/color/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export * as color from './color';
export * as colorspace from './colorspace';
export * as hsl from './hsl';

export type { Color, ColorInput } from './color';
export type { HSL } from './hsl';
export type { Color, ColorInput, RColor } from './color';
export type { HSL, RHSL } from './hsl';
17 changes: 10 additions & 7 deletions src/core/euler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Mat4 } from './mat4';
import type { Quat } from './quat';
import type { RMat4 } from './mat4';
import type { RQuat } from './quat';
import * as quat from './quat';
import { clamp, EPSILON } from './scalar';

Expand All @@ -11,6 +11,9 @@ export type EulerOrder = 'xyz' | 'xzy' | 'yxz' | 'yzx' | 'zxy' | 'zyx';
/** A Euler in 3D space, with an optional order (default is 'xyz') */
export type Euler = [x: number, y: number, z: number, order?: EulerOrder];

/** A read-only set of Euler angles */
export type REuler = Readonly<Euler>;

/**
* Creates a new Euler with default values (0, 0, 0, 'xyz').
*/
Expand Down Expand Up @@ -71,7 +74,7 @@ export function fromDegrees(out: Euler, x: number, y: number, z: number, order:
* @param order The order of the Euler angles.
* @returns The output Euler.
*/
export function fromRotationMat4(out: Euler, rotationMatrix: Mat4, order: EulerOrder = out[3] || 'xyz'): Euler {
export function fromRotationMat4(out: Euler, rotationMatrix: RMat4, order: EulerOrder = out[3] || 'xyz'): Euler {
return fromRotationMatrixValues(
out,
rotationMatrix[0],
Expand Down Expand Up @@ -199,7 +202,7 @@ function fromRotationMatrixValues(
* @param b The second euler.
* @returns True if the euler angles are equal, false otherwise.
*/
export function exactEquals(a: Euler, b: Euler): boolean {
export function exactEquals(a: REuler, b: REuler): boolean {
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
}

Expand All @@ -210,7 +213,7 @@ export function exactEquals(a: Euler, b: Euler): boolean {
* @param b The second euler.
* @returns True if the euler angles are equal, false otherwise.
*/
export function equals(a: Euler, b: Euler): boolean {
export function equals(a: REuler, b: REuler): boolean {
const a0 = a[0];
const a1 = a[1];
const a2 = a[2];
Expand All @@ -232,7 +235,7 @@ export function equals(a: Euler, b: Euler): boolean {
* @param order The order of the Euler.
* @returns The output Euler
*/
export function fromQuat(out: Euler, q: Quat, order: EulerOrder): Euler {
export function fromQuat(out: Euler, q: RQuat, order: EulerOrder): Euler {
// compute the rotation matrix elements directly from the quaternion
const x = q[0];
const y = q[1];
Expand Down Expand Up @@ -276,7 +279,7 @@ const _reorderQuaternion = /*@__PURE__*/ quat.create();
* @param order The order of the Euler.
* @returns The output Euler.
*/
export function reorder(out: Euler, a: Euler, order: EulerOrder): Euler {
export function reorder(out: Euler, a: REuler, order: EulerOrder): Euler {
quat.fromEuler(_reorderQuaternion, a);
fromQuat(out, _reorderQuaternion, order);
return out;
Expand Down
25 changes: 13 additions & 12 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
export type { DeepReadonly } from './readonly';
export * from './scalar';
export * from './angle';
export type { MutableArrayLike } from './arrays';

export * as vec2 from './vec2';
export type { Vec2 } from './vec2';
export type { RVec2, Vec2 } from './vec2';

export * as vec3 from './vec3';
export type { Vec3 } from './vec3';
export type { RVec3, Vec3 } from './vec3';

export * as vec4 from './vec4';
export type { Vec4 } from './vec4';
export type { RVec4, Vec4 } from './vec4';

export * as euler from './euler';
export type { Euler, EulerOrder } from './euler';
export type { Euler, EulerOrder, REuler } from './euler';

export * as quat from './quat';
export type { Quat } from './quat';
export type { Quat, RQuat } from './quat';

export * as quat2 from './quat2';
export type { Quat2 } from './quat2';
export type { Quat2, RQuat2 } from './quat2';

export * as mat2 from './mat2';
export type { Mat2 } from './mat2';
export type { Mat2, RMat2 } from './mat2';

export * as mat2d from './mat2d';
export type { Mat2d } from './mat2d';
export type { Mat2d, RMat2d } from './mat2d';

export * as mat3 from './mat3';
export type { Mat3 } from './mat3';
export type { Mat3, RMat3 } from './mat3';

export * as mat4 from './mat4';
export type { Mat4 } from './mat4';
export type { Mat4, RMat4 } from './mat4';

export * as spherical from './spherical';
export type { Spherical } from './spherical';
export type { RSpherical, Spherical } from './spherical';

export * as polar from './polar';
export type { Polar } from './polar';
export type { Polar, RPolar } from './polar';
Loading