@forgeax/engine-math 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +294 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/_arbs.d.ts +36 -0
- package/dist/__tests__/_arbs.d.ts.map +1 -0
- package/dist/__tests__/_fixtures.d.ts +61 -0
- package/dist/__tests__/_fixtures.d.ts.map +1 -0
- package/dist/__tests__/bounds2.test.d.ts +2 -0
- package/dist/__tests__/bounds2.test.d.ts.map +1 -0
- package/dist/__tests__/box3.test.d.ts +2 -0
- package/dist/__tests__/box3.test.d.ts.map +1 -0
- package/dist/__tests__/easing.test.d.ts +2 -0
- package/dist/__tests__/easing.test.d.ts.map +1 -0
- package/dist/__tests__/euler.test-d.d.ts +2 -0
- package/dist/__tests__/euler.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat3.test-d.d.ts +2 -0
- package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat4.property.test.d.ts +2 -0
- package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
- package/dist/__tests__/mat4.test-d.d.ts +2 -0
- package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
- package/dist/__tests__/mat4.test.d.ts +2 -0
- package/dist/__tests__/mat4.test.d.ts.map +1 -0
- package/dist/__tests__/noise.test.d.ts +2 -0
- package/dist/__tests__/noise.test.d.ts.map +1 -0
- package/dist/__tests__/quat.basis.test.d.ts +2 -0
- package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
- package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
- package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
- package/dist/__tests__/quat.lookat.test.d.ts +2 -0
- package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
- package/dist/__tests__/quat.property.test.d.ts +2 -0
- package/dist/__tests__/quat.property.test.d.ts.map +1 -0
- package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
- package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
- package/dist/__tests__/quat.test-d.d.ts +2 -0
- package/dist/__tests__/quat.test-d.d.ts.map +1 -0
- package/dist/__tests__/ray.property.test.d.ts +2 -0
- package/dist/__tests__/ray.property.test.d.ts.map +1 -0
- package/dist/__tests__/ray.test.d.ts +2 -0
- package/dist/__tests__/ray.test.d.ts.map +1 -0
- package/dist/__tests__/types.test-d.d.ts +2 -0
- package/dist/__tests__/types.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
- package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
- package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
- package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
- package/dist/__tests__/vec2.test-d.d.ts +2 -0
- package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec3.property.test.d.ts +2 -0
- package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
- package/dist/__tests__/vec3.test-d.d.ts +2 -0
- package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
- package/dist/__tests__/vec4.test-d.d.ts +2 -0
- package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
- package/dist/_internal/epsilon.d.ts +9 -0
- package/dist/_internal/epsilon.d.ts.map +1 -0
- package/dist/_internal/scalar.d.ts +40 -0
- package/dist/_internal/scalar.d.ts.map +1 -0
- package/dist/box2.d.ts +35 -0
- package/dist/box2.d.ts.map +1 -0
- package/dist/box3.d.ts +76 -0
- package/dist/box3.d.ts.map +1 -0
- package/dist/circle2.d.ts +29 -0
- package/dist/circle2.d.ts.map +1 -0
- package/dist/color.d.ts +64 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/easing.d.ts +18 -0
- package/dist/easing.d.ts.map +1 -0
- package/dist/euler.d.ts +51 -0
- package/dist/euler.d.ts.map +1 -0
- package/dist/f32-to-f16-bytes.d.ts +10 -0
- package/dist/f32-to-f16-bytes.d.ts.map +1 -0
- package/dist/frustum.d.ts +49 -0
- package/dist/frustum.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3688 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mat3.d.ts +70 -0
- package/dist/mat3.d.ts.map +1 -0
- package/dist/mat4.d.ts +329 -0
- package/dist/mat4.d.ts.map +1 -0
- package/dist/noise.d.ts +12 -0
- package/dist/noise.d.ts.map +1 -0
- package/dist/quat.d.ts +312 -0
- package/dist/quat.d.ts.map +1 -0
- package/dist/ray.d.ts +144 -0
- package/dist/ray.d.ts.map +1 -0
- package/dist/ray2.d.ts +38 -0
- package/dist/ray2.d.ts.map +1 -0
- package/dist/sphere.d.ts +41 -0
- package/dist/sphere.d.ts.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vec2.d.ts +76 -0
- package/dist/vec2.d.ts.map +1 -0
- package/dist/vec3.d.ts +82 -0
- package/dist/vec3.d.ts.map +1 -0
- package/dist/vec4.d.ts +61 -0
- package/dist/vec4.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/_arbs.ts +149 -0
- package/src/__tests__/_fixtures.ts +118 -0
- package/src/__tests__/bounds2.test.ts +146 -0
- package/src/__tests__/box3.test.ts +277 -0
- package/src/__tests__/easing.test.ts +109 -0
- package/src/__tests__/euler.test-d.ts +63 -0
- package/src/__tests__/mat3.test-d.ts +47 -0
- package/src/__tests__/mat4.property.test.ts +256 -0
- package/src/__tests__/mat4.test-d.ts +130 -0
- package/src/__tests__/mat4.test.ts +162 -0
- package/src/__tests__/noise.test.ts +75 -0
- package/src/__tests__/quat.basis.test.ts +134 -0
- package/src/__tests__/quat.interpolation.test.ts +45 -0
- package/src/__tests__/quat.lookat.test.ts +103 -0
- package/src/__tests__/quat.property.test.ts +150 -0
- package/src/__tests__/quat.rotateaxis.test.ts +149 -0
- package/src/__tests__/quat.test-d.ts +138 -0
- package/src/__tests__/ray.property.test.ts +105 -0
- package/src/__tests__/ray.test.ts +539 -0
- package/src/__tests__/types.test-d.ts +64 -0
- package/src/__tests__/vec-catmull-rom.test.ts +125 -0
- package/src/__tests__/vec-smooth-damp.test.ts +167 -0
- package/src/__tests__/vec2.test-d.ts +59 -0
- package/src/__tests__/vec3.property.test.ts +72 -0
- package/src/__tests__/vec3.test-d.ts +65 -0
- package/src/__tests__/vec4.test-d.ts +61 -0
- package/src/_internal/epsilon.ts +29 -0
- package/src/_internal/scalar.ts +90 -0
- package/src/box2.ts +168 -0
- package/src/box3.ts +372 -0
- package/src/circle2.ts +134 -0
- package/src/color.ts +177 -0
- package/src/easing.ts +49 -0
- package/src/euler.ts +239 -0
- package/src/f32-to-f16-bytes.ts +71 -0
- package/src/frustum.ts +240 -0
- package/src/index.ts +65 -0
- package/src/mat3.ts +286 -0
- package/src/mat4.ts +1334 -0
- package/src/noise.ts +78 -0
- package/src/quat.ts +847 -0
- package/src/ray.ts +575 -0
- package/src/ray2.ts +198 -0
- package/src/sphere.ts +138 -0
- package/src/types.ts +78 -0
- package/src/vec2.ts +229 -0
- package/src/vec3.ts +294 -0
- package/src/vec4.ts +235 -0
package/src/color.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// color.ts — RGBA color namespace (M5 / T-032)
|
|
2
|
+
//
|
|
3
|
+
// 6-function surface: create / clone / srgbToLinear / linearToSrgb / fromHex / toHex
|
|
4
|
+
//
|
|
5
|
+
// Design anchors:
|
|
6
|
+
// - branded Float32Array length 4 [r, g, b, a]; `as Color` casts are funneled inside factories (D-P15)
|
|
7
|
+
// - sRGB ↔ linear: IEC 61966-2-1 piecewise gamma; RGB channels only, alpha pass-through
|
|
8
|
+
// - srgbToLinear: cutoff 0.04045 / linear segment v/12.92 / power segment ((v+0.055)/1.055)^2.4
|
|
9
|
+
// - linearToSrgb: cutoff 0.0031308 / linear segment 12.92*v / power segment 1.055*v^(1/2.4) - 0.055
|
|
10
|
+
// - negatives / NaN returned verbatim (HDR-friendly + IEEE-754 NaN propagation)
|
|
11
|
+
// - fromHex (D-P7): only `#RRGGBB` (7 chars) and `#RRGGBBAA` (9 chars); illegal inputs silently fall
|
|
12
|
+
// back to (0, 0, 0, 1) without throwing (D-P12 degenerate family / AC-06)
|
|
13
|
+
// - #RGB / #RGBA short forms are not supported (stricter than bevy_color::Srgba::hex; less ambiguous
|
|
14
|
+
// in practice; wiki/sources/2026-05-05-bevy-0-19-math-transform-color §Srgba::hex mentions short
|
|
15
|
+
// forms but D-P7 actively tightens the contract)
|
|
16
|
+
// - toHex: alpha=1 → `#rrggbb`; alpha<1 → `#rrggbbaa`; components are clamped to [0, 1] then
|
|
17
|
+
// multiplied by 255 and rounded; output is lowercase, symmetric with fromHex's tolerant input
|
|
18
|
+
//
|
|
19
|
+
// Related: requirements §Surface color lower bound 6 + AC-06 silent fall-back (never raises);
|
|
20
|
+
// plan-strategy D-P7 / D-P12 / §appendix A degenerate registry #14-#16;
|
|
21
|
+
// wiki/sources/2026-05-05-bevy-0-19-math-transform-color §sRGB piecewise gamma;
|
|
22
|
+
// wiki/glam-rs-overview §LinearRgba.
|
|
23
|
+
|
|
24
|
+
import type { Color, ColorLike } from './types';
|
|
25
|
+
|
|
26
|
+
export type { Color, ColorLike };
|
|
27
|
+
|
|
28
|
+
// === create / copy ===
|
|
29
|
+
|
|
30
|
+
/** Create a Color (default RGBA = (0, 0, 0, 1) = opaque black). */
|
|
31
|
+
export function create(r = 0, g = 0, b = 0, a = 1): Color {
|
|
32
|
+
return Float32Array.of(r, g, b, a) as Color;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Allocate a new Color copy. */
|
|
36
|
+
export function clone(c: ColorLike): Color {
|
|
37
|
+
return Float32Array.of(c[0] as number, c[1] as number, c[2] as number, c[3] as number) as Color;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// === sRGB ↔ linear (IEC 61966-2-1 piecewise gamma) ===
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Single-channel sRGB → linear (component-level helper).
|
|
44
|
+
*
|
|
45
|
+
* @degrade NaN input → NaN output (IEEE-754 arithmetic propagation); negatives returned verbatim
|
|
46
|
+
* to preserve HDR / specialized rendering needs (same convention as bevy_color::gamma_function;
|
|
47
|
+
* wiki/sources bevy-0-19-color §gamma_function).
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* srgbChannelToLinear(NaN); // → NaN (IEEE-754 propagation)
|
|
52
|
+
* srgbChannelToLinear(-0.1); // → -0.1 (HDR / negative kept verbatim)
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function srgbChannelToLinear(v: number): number {
|
|
56
|
+
if (Number.isNaN(v)) return Number.NaN;
|
|
57
|
+
if (v <= 0) return v;
|
|
58
|
+
if (v <= 0.04045) return v / 12.92;
|
|
59
|
+
return ((v + 0.055) / 1.055) ** 2.4;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Single-channel linear → sRGB (component-level helper). */
|
|
63
|
+
function linearChannelToSrgb(v: number): number {
|
|
64
|
+
if (Number.isNaN(v)) return Number.NaN;
|
|
65
|
+
if (v <= 0) return v;
|
|
66
|
+
if (v <= 0.0031308) return v * 12.92;
|
|
67
|
+
return 1.055 * v ** (1 / 2.4) - 0.055;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* out = sRGB → linear conversion (RGB channels only; alpha passed through).
|
|
72
|
+
*
|
|
73
|
+
* @degrade NaN propagation / negatives returned verbatim (HDR-friendly); alpha is strictly untouched.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* color.srgbToLinear(out, color.create(NaN, -0.1, 0.5, 0.8));
|
|
78
|
+
* // → out = (NaN, -0.1, ~0.214, 0.8); alpha 0.8 passed through unchanged.
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function srgbToLinear(out: Color, c: ColorLike): Color {
|
|
82
|
+
out[0] = srgbChannelToLinear(c[0] as number);
|
|
83
|
+
out[1] = srgbChannelToLinear(c[1] as number);
|
|
84
|
+
out[2] = srgbChannelToLinear(c[2] as number);
|
|
85
|
+
out[3] = c[3] as number;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* out = linear → sRGB conversion (RGB channels only; alpha passed through).
|
|
91
|
+
*
|
|
92
|
+
* @degrade NaN propagation / negatives returned verbatim; alpha is strictly untouched.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* color.linearToSrgb(out, color.create(NaN, -0.05, 0.5, 1));
|
|
97
|
+
* // → out = (NaN, -0.05, ~0.735, 1); alpha is strictly untouched.
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function linearToSrgb(out: Color, c: ColorLike): Color {
|
|
101
|
+
out[0] = linearChannelToSrgb(c[0] as number);
|
|
102
|
+
out[1] = linearChannelToSrgb(c[1] as number);
|
|
103
|
+
out[2] = linearChannelToSrgb(c[2] as number);
|
|
104
|
+
out[3] = c[3] as number;
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// === Hex parse / serialize ===
|
|
109
|
+
|
|
110
|
+
const HEX_PATTERN = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?$/;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Write a Color from a hex string.
|
|
114
|
+
*
|
|
115
|
+
* Only supports the two forms `#RRGGBB` (7 chars) and `#RRGGBBAA` (9 chars) (D-P7);
|
|
116
|
+
* the CSS-style `#RGB` / `#RGBA` short forms are not supported.
|
|
117
|
+
*
|
|
118
|
+
* @degrade Any illegal input (short form / non-hex chars / missing # / wrong length / empty string,
|
|
119
|
+
* etc.) silently falls back to (0, 0, 0, 1); never throws (D-P12 / AC-06).
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* color.fromHex(out, '#ff8000'); // → (1, 0.502, 0, 1)
|
|
124
|
+
* color.fromHex(out, '#ff800080'); // → (1, 0.502, 0, 0.502)
|
|
125
|
+
* color.fromHex(out, '#fff'); // → (0, 0, 0, 1) short form unsupported → silent fall-back
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function fromHex(out: Color, hex: string): Color {
|
|
129
|
+
const match = typeof hex === 'string' ? HEX_PATTERN.exec(hex) : null;
|
|
130
|
+
if (match === null) {
|
|
131
|
+
out[0] = 0;
|
|
132
|
+
out[1] = 0;
|
|
133
|
+
out[2] = 0;
|
|
134
|
+
out[3] = 1;
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
out[0] = Number.parseInt(match[1] as string, 16) / 255;
|
|
138
|
+
out[1] = Number.parseInt(match[2] as string, 16) / 255;
|
|
139
|
+
out[2] = Number.parseInt(match[3] as string, 16) / 255;
|
|
140
|
+
out[3] = match[4] !== undefined ? Number.parseInt(match[4], 16) / 255 : 1;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Per-component [0, 1] clamp + ×255 + round + 2-digit hex (lowercase). */
|
|
145
|
+
function toHexByte(v: number): string {
|
|
146
|
+
if (Number.isNaN(v)) return '00';
|
|
147
|
+
const clamped = Math.max(0, Math.min(1, v));
|
|
148
|
+
const byte = Math.round(clamped * 255);
|
|
149
|
+
return byte.toString(16).padStart(2, '0');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serialize as hex string: alpha=1 → `#rrggbb`; alpha<1 → `#rrggbbaa`.
|
|
154
|
+
*
|
|
155
|
+
* @degrade Out-of-range components (HDR > 1 / negatives) are clamped to [0, 1] then rounded;
|
|
156
|
+
* **truncated** rather than throwing (symmetric with fromHex's silent fall-back). NaN components →
|
|
157
|
+
* literal '00' (same byte as 0.0; prevents the hex-string builder from throwing; alpha=NaN takes the
|
|
158
|
+
* alpha=1 path, emitting the short `#rrggbb` form).
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* color.toHex(color.create(2, -0.1, 0.5, 1)); // → '#ff0080' (HDR/negative clamp)
|
|
163
|
+
* color.toHex(color.create(1, 0, 0, 0.5)); // → '#ff000080'
|
|
164
|
+
* color.toHex(color.create(NaN, 0, 0, NaN)); // → '#000000' (NaN→'00', alpha=NaN takes short form)
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function toHex(c: ColorLike): string {
|
|
168
|
+
const r = toHexByte(c[0] as number);
|
|
169
|
+
const g = toHexByte(c[1] as number);
|
|
170
|
+
const b = toHexByte(c[2] as number);
|
|
171
|
+
const aValue = c[3] as number;
|
|
172
|
+
if (aValue >= 1 || Number.isNaN(aValue)) {
|
|
173
|
+
return `#${r}${g}${b}`;
|
|
174
|
+
}
|
|
175
|
+
const a = toHexByte(aValue);
|
|
176
|
+
return `#${r}${g}${b}${a}`;
|
|
177
|
+
}
|
package/src/easing.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// easing.ts — easing-function namespace (solo round 20260713-233409)
|
|
2
|
+
//
|
|
3
|
+
// 4-function surface: cubicInOut / smoothstep / smootherstep / elasticInOut
|
|
4
|
+
// (GLSL `smoothstep`, Perlin's smootherstep; Bevy `EaseFunction::SmoothStep`/`SmootherStep`).
|
|
5
|
+
// Scalar time-remaps t → number: take a normalized parameter and return an eased value. The
|
|
6
|
+
// growable home for Bevy's `EaseFunction` family — further variants (sine / quad / bounce /
|
|
7
|
+
// steps) land here add-only.
|
|
8
|
+
//
|
|
9
|
+
// Every function clamps the input to [0, 1] first (GLSL / Bevy semantics), so out-of-range t
|
|
10
|
+
// saturates to the endpoints rather than extrapolating the curve.
|
|
11
|
+
|
|
12
|
+
import { clamp } from './_internal/scalar';
|
|
13
|
+
|
|
14
|
+
/** Cubic ease-in-out: Bevy `EaseFunction::CubicInOut`, clamped to [0, 1]. */
|
|
15
|
+
export function cubicInOut(t: number): number {
|
|
16
|
+
const x = clamp(t, 0, 1);
|
|
17
|
+
return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Smoothstep S-curve: `3t² − 2t³` on the clamped input. GLSL `smoothstep` (with edges 0/1),
|
|
22
|
+
* Bevy `EaseFunction::SmoothStep`. f(0)=0, f(1)=1, f′(0)=f′(1)=0 (slow-in / slow-out).
|
|
23
|
+
* Input clamped to [0, 1]. Use to ease a normalized time / lerp factor instead of a linear ramp.
|
|
24
|
+
*/
|
|
25
|
+
export function smoothstep(t: number): number {
|
|
26
|
+
const x = clamp(t, 0, 1);
|
|
27
|
+
return x * x * (3 - 2 * x);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Smootherstep (Perlin) S-curve: `6t⁵ − 15t⁴ + 10t³` on the clamped input. Bevy
|
|
32
|
+
* `EaseFunction::SmootherStep`. Like {@link smoothstep} but ALSO has zero 2nd derivatives at
|
|
33
|
+
* the endpoints (f″(0)=f″(1)=0), so acceleration is continuous — a gentler, more natural ease.
|
|
34
|
+
* Input clamped to [0, 1].
|
|
35
|
+
*/
|
|
36
|
+
export function smootherstep(t: number): number {
|
|
37
|
+
const x = clamp(t, 0, 1);
|
|
38
|
+
return x * x * x * (x * (x * 6 - 15) + 10);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Elastic ease-in-out: Bevy `EaseFunction::ElasticInOut`, clamped to [0, 1]. */
|
|
42
|
+
export function elasticInOut(t: number): number {
|
|
43
|
+
const x = clamp(t, 0, 1);
|
|
44
|
+
if (x === 0 || x === 1) return x;
|
|
45
|
+
const c5 = (2 * Math.PI) / 4.5;
|
|
46
|
+
return x < 0.5
|
|
47
|
+
? -(2 ** (20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2
|
|
48
|
+
: (2 ** (-20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1;
|
|
49
|
+
}
|
package/src/euler.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// euler.ts — Euler angle namespace (M4 / T-028)
|
|
2
|
+
//
|
|
3
|
+
// 6-function surface (≥ 6 lower bound):
|
|
4
|
+
// create / clone / set / fromQuat / toQuat / fromRotationMatrix
|
|
5
|
+
//
|
|
6
|
+
// Memory layout: plain object `{ x: number, y: number, z: number, order: EulerOrder }`
|
|
7
|
+
// (not a Float32Array). Rationale:
|
|
8
|
+
// - need to store length=3 plus a string order; strings cannot live inside a TypedArray
|
|
9
|
+
// - performance-insensitive (euler is only used at the editor/IO boundary; runtime always converts to Quat)
|
|
10
|
+
//
|
|
11
|
+
// 6 orders (intrinsic rotation; literal order x → y → z):
|
|
12
|
+
// XYZ / YXZ / ZXY / ZYX / YZX / XZY
|
|
13
|
+
//
|
|
14
|
+
// Degenerate convention (plan-strategy §appendix A degenerate registry #16):
|
|
15
|
+
// - fromQuat(q, order) gimbal-lock (pitch ≈ ±π/2) → pick an equivalent branch; never throws
|
|
16
|
+
// - fromRotationMatrix follows the same convention
|
|
17
|
+
//
|
|
18
|
+
// Related: requirements §Surface euler lower bound 6 + 6-order full support;
|
|
19
|
+
// plan-strategy §1.1 euler.ts + §appendix A degenerate registry #16 + D-P19 EulerOrder union;
|
|
20
|
+
// wiki/glam-rs-overview Hamilton convention +
|
|
21
|
+
// Three.js Euler.setFromQuaternion / setFromRotationMatrix 6-order formulas.
|
|
22
|
+
//
|
|
23
|
+
// Degenerate-semantics registry (plan-strategy.md §appendix A; D-P16 dual promise:
|
|
24
|
+
// runtime silent + JSDoc `@degrade` + `@example` guard pattern landing together):
|
|
25
|
+
// #16 euler.fromQuat(q, order) gimbal-lock → pick equivalent branch (no throw)
|
|
26
|
+
// #17 euler.fromRotationMatrix gimbal-lock → same as above
|
|
27
|
+
|
|
28
|
+
import * as quat from './quat';
|
|
29
|
+
import type { Euler, EulerOrder, Mat3Like, Quat, QuatLike } from './types';
|
|
30
|
+
|
|
31
|
+
export type { Euler, EulerOrder };
|
|
32
|
+
|
|
33
|
+
/** Create an Euler, defaulting to (0, 0, 0, 'XYZ'). */
|
|
34
|
+
export function create(): Euler {
|
|
35
|
+
return { x: 0, y: 0, z: 0, order: 'XYZ' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Allocate a new Euler copy. */
|
|
39
|
+
export function clone(a: Euler): Euler {
|
|
40
|
+
return { x: a.x, y: a.y, z: a.z, order: a.order };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** out.x/y/z/order = inputs; returns out (in-place). */
|
|
44
|
+
export function set(out: Euler, x: number, y: number, z: number, order: EulerOrder): Euler {
|
|
45
|
+
out.x = x;
|
|
46
|
+
out.y = y;
|
|
47
|
+
out.z = z;
|
|
48
|
+
out.order = order;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* out (Quat) = quaternion from euler. Returns out.
|
|
54
|
+
*
|
|
55
|
+
* Routes directly to quat.fromEuler (D-P2 + 6-order coverage already implemented inside quat).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* euler.toQuat(quatBuf, eulerInstance);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function toQuat(out: Quat, e: Euler): Quat {
|
|
63
|
+
return quat.fromEuler(out, e.x, e.y, e.z, e.order);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* out (Euler) = euler angles from quaternion in given order. Returns out.
|
|
68
|
+
*
|
|
69
|
+
* Implementation path: quat → 3x3 column-major rotation matrix → fromRotationMatrix(order).
|
|
70
|
+
* This concentrates the 6-order formulas in fromRotationMatrix and avoids dual maintenance.
|
|
71
|
+
*
|
|
72
|
+
* @degrade gimbal lock (pitch ≈ ±π/2, sin/cos critical) → set yaw to 0 and let roll absorb the
|
|
73
|
+
* full rotation (registry #16; same equivalent-branch semantics as Three.js; no throw).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* euler.fromQuat(out, q, 'XYZ');
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function fromQuat(out: Euler, q: QuatLike, order: EulerOrder): Euler {
|
|
81
|
+
// Expand q into a 3x3 column-major rotation matrix (matching the index convention of quat.fromRotationMatrix)
|
|
82
|
+
const x = q[0] as number;
|
|
83
|
+
const y = q[1] as number;
|
|
84
|
+
const z = q[2] as number;
|
|
85
|
+
const w = q[3] as number;
|
|
86
|
+
const xx = x * x;
|
|
87
|
+
const xy = x * y;
|
|
88
|
+
const xz = x * z;
|
|
89
|
+
const yy = y * y;
|
|
90
|
+
const yz = y * z;
|
|
91
|
+
const zz = z * z;
|
|
92
|
+
const wx = w * x;
|
|
93
|
+
const wy = w * y;
|
|
94
|
+
const wz = w * z;
|
|
95
|
+
|
|
96
|
+
// mat3 column-major: m[col*3+row] → m00=col0row0, m01=col0row1, m10=col1row0...
|
|
97
|
+
// R[row][col] maps to m[col*3+row]:
|
|
98
|
+
// R[0][0]=m00,R[0][1]=m10,R[0][2]=m20
|
|
99
|
+
// R[1][0]=m01,R[1][1]=m11,R[1][2]=m21
|
|
100
|
+
// R[2][0]=m02,R[2][1]=m12,R[2][2]=m22
|
|
101
|
+
const m = new Float32Array(9);
|
|
102
|
+
m[0] = 1 - 2 * (yy + zz);
|
|
103
|
+
m[1] = 2 * (xy + wz);
|
|
104
|
+
m[2] = 2 * (xz - wy);
|
|
105
|
+
m[3] = 2 * (xy - wz);
|
|
106
|
+
m[4] = 1 - 2 * (xx + zz);
|
|
107
|
+
m[5] = 2 * (yz + wx);
|
|
108
|
+
m[6] = 2 * (xz + wy);
|
|
109
|
+
m[7] = 2 * (yz - wx);
|
|
110
|
+
m[8] = 1 - 2 * (xx + yy);
|
|
111
|
+
|
|
112
|
+
return fromRotationMatrix(out, m, order);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* out (Euler) = euler from a 3x3 rotation matrix m (column-major, length 9) in `order`. Returns out.
|
|
117
|
+
*
|
|
118
|
+
* Uses the Three.js Euler.setFromRotationMatrix formulas (intrinsic rotation + Hamilton convention).
|
|
119
|
+
* Each of the 6 orders has an analytic formula; near gimbal-lock (middle axis sin/cos near ±1)
|
|
120
|
+
* an equivalent branch is selected.
|
|
121
|
+
*
|
|
122
|
+
* @degrade gimbal lock → middle axis is fixed at ±π/2; the remaining two axes degenerate
|
|
123
|
+
* (one is set to 0; the other absorbs the rotation).
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* euler.fromRotationMatrix(out, mat3InstanceColumnMajor, 'XYZ');
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function fromRotationMatrix(out: Euler, m: Mat3Like, order: EulerOrder): Euler {
|
|
131
|
+
// Column-major indexing (matches quat.fromRotationMatrix):
|
|
132
|
+
// m00 m01 m02 = column 0 (col=0)'s row=0/1/2
|
|
133
|
+
// m10 m11 m12 = column 1 (col=1)'s row=0/1/2
|
|
134
|
+
// m20 m21 m22 = column 2 (col=2)'s row=0/1/2
|
|
135
|
+
// Mathematically R[row][col] = m[c*3 + r].
|
|
136
|
+
// Thus, in the Three.js formulas, m11 (R[0][0]) corresponds to our m[0],
|
|
137
|
+
// m12 (R[0][1]) corresponds to m[3],
|
|
138
|
+
// m13 (R[0][2]) corresponds to m[6],
|
|
139
|
+
// m21 (R[1][0]) corresponds to m[1], etc.
|
|
140
|
+
// For readability and alignment with Three.js naming, alias (_ij denotes R[i-1][j-1]):
|
|
141
|
+
const _11 = m[0] as number;
|
|
142
|
+
const _21 = m[1] as number;
|
|
143
|
+
const _31 = m[2] as number;
|
|
144
|
+
const _12 = m[3] as number;
|
|
145
|
+
const _22 = m[4] as number;
|
|
146
|
+
const _32 = m[5] as number;
|
|
147
|
+
const _13 = m[6] as number;
|
|
148
|
+
const _23 = m[7] as number;
|
|
149
|
+
const _33 = m[8] as number;
|
|
150
|
+
|
|
151
|
+
out.order = order;
|
|
152
|
+
switch (order) {
|
|
153
|
+
case 'XYZ':
|
|
154
|
+
out.y = Math.asin(clamp11(_13));
|
|
155
|
+
if (Math.abs(_13) < 1 - 1e-7) {
|
|
156
|
+
out.x = Math.atan2(-_23, _33);
|
|
157
|
+
out.z = Math.atan2(-_12, _11);
|
|
158
|
+
} else {
|
|
159
|
+
// gimbal lock
|
|
160
|
+
out.x = Math.atan2(_32, _22);
|
|
161
|
+
out.z = 0;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case 'YXZ':
|
|
165
|
+
out.x = Math.asin(-clamp11(_23));
|
|
166
|
+
if (Math.abs(_23) < 1 - 1e-7) {
|
|
167
|
+
out.y = Math.atan2(_13, _33);
|
|
168
|
+
out.z = Math.atan2(_21, _22);
|
|
169
|
+
} else {
|
|
170
|
+
out.y = Math.atan2(-_31, _11);
|
|
171
|
+
out.z = 0;
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case 'ZXY':
|
|
175
|
+
out.x = Math.asin(clamp11(_32));
|
|
176
|
+
if (Math.abs(_32) < 1 - 1e-7) {
|
|
177
|
+
out.y = Math.atan2(-_31, _33);
|
|
178
|
+
out.z = Math.atan2(-_12, _22);
|
|
179
|
+
} else {
|
|
180
|
+
out.y = 0;
|
|
181
|
+
out.z = Math.atan2(_21, _11);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'ZYX':
|
|
185
|
+
out.y = Math.asin(-clamp11(_31));
|
|
186
|
+
if (Math.abs(_31) < 1 - 1e-7) {
|
|
187
|
+
out.x = Math.atan2(_32, _33);
|
|
188
|
+
out.z = Math.atan2(_21, _11);
|
|
189
|
+
} else {
|
|
190
|
+
out.x = 0;
|
|
191
|
+
out.z = Math.atan2(-_12, _22);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'YZX':
|
|
195
|
+
out.z = Math.asin(clamp11(_21));
|
|
196
|
+
if (Math.abs(_21) < 1 - 1e-7) {
|
|
197
|
+
out.x = Math.atan2(-_23, _22);
|
|
198
|
+
out.y = Math.atan2(-_31, _11);
|
|
199
|
+
} else {
|
|
200
|
+
out.x = 0;
|
|
201
|
+
out.y = Math.atan2(_13, _33);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case 'XZY':
|
|
205
|
+
out.z = Math.asin(-clamp11(_12));
|
|
206
|
+
if (Math.abs(_12) < 1 - 1e-7) {
|
|
207
|
+
out.x = Math.atan2(_32, _22);
|
|
208
|
+
out.y = Math.atan2(_13, _11);
|
|
209
|
+
} else {
|
|
210
|
+
out.x = Math.atan2(-_23, _33);
|
|
211
|
+
out.y = 0;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
// Silent fallback to 'XYZ' (same convention as quat.fromEuler D-P2)
|
|
216
|
+
out.y = Math.asin(clamp11(_13));
|
|
217
|
+
if (Math.abs(_13) < 1 - 1e-7) {
|
|
218
|
+
out.x = Math.atan2(-_23, _33);
|
|
219
|
+
out.z = Math.atan2(-_12, _11);
|
|
220
|
+
} else {
|
|
221
|
+
out.x = Math.atan2(_32, _22);
|
|
222
|
+
out.z = 0;
|
|
223
|
+
}
|
|
224
|
+
out.order = 'XYZ';
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Internal: clamp to [-1, 1] to keep asin from receiving out-of-range inputs (floating-point error
|
|
233
|
+
* may make |x| slightly > 1; same guard as Three.js; not counted in the surface).
|
|
234
|
+
*/
|
|
235
|
+
function clamp11(v: number): number {
|
|
236
|
+
if (v < -1) return -1;
|
|
237
|
+
if (v > 1) return 1;
|
|
238
|
+
return v;
|
|
239
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// f32-to-f16-bytes.ts — IEEE 754 binary16 half-float conversion.
|
|
2
|
+
//
|
|
3
|
+
// Pure arithmetic codec extracted from runtime/render-resource-table.ts (D-3)
|
|
4
|
+
// so build-time image-importer and runtime uploadCubemapFromEquirect share
|
|
5
|
+
// a single SSOT. Takes a Uint8Array view over packed Float32 RGBA pixels
|
|
6
|
+
// and returns a Uint8Array with half the byte length of the equivalent
|
|
7
|
+
// binary16 interleaved pixels (little-endian).
|
|
8
|
+
//
|
|
9
|
+
// Out-of-range f32 values saturate to +/-inf, NaN propagates, and subnormals
|
|
10
|
+
// round-to-zero (industry-standard f32->f16 path, semantic-preserving
|
|
11
|
+
// extraction).
|
|
12
|
+
//
|
|
13
|
+
// Related: plan-strategy §2 D-3; research Finding 5 + C3; requirements C3;
|
|
14
|
+
// w1 test file: packages/math/src/__tests__/f32-to-f16-bytes.test.ts.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a packed float32 RGBA byte buffer into the equivalent float16 RGBA
|
|
18
|
+
* byte buffer (IEEE 754 binary16, little-endian). The output length is
|
|
19
|
+
* `src.byteLength / 2`.
|
|
20
|
+
*
|
|
21
|
+
* @param src A Uint8Array whose underlying buffer is an interleaved Float32Array.
|
|
22
|
+
* @returns A Uint8Array of packed binary16 pixels, exactly half the length.
|
|
23
|
+
*/
|
|
24
|
+
export function f32ToF16Bytes(src: Uint8Array): Uint8Array {
|
|
25
|
+
const f32 = new Float32Array(src.buffer, src.byteOffset, src.byteLength / 4);
|
|
26
|
+
const out = new Uint8Array(f32.length * 2);
|
|
27
|
+
const view = new DataView(out.buffer);
|
|
28
|
+
const scratch = new ArrayBuffer(4);
|
|
29
|
+
const scratchF = new Float32Array(scratch);
|
|
30
|
+
const scratchU = new Uint32Array(scratch);
|
|
31
|
+
for (let i = 0; i < f32.length; i++) {
|
|
32
|
+
scratchF[0] = f32[i] ?? 0;
|
|
33
|
+
const bits = scratchU[0] ?? 0;
|
|
34
|
+
const sign = (bits >>> 31) & 0x1;
|
|
35
|
+
const exp = (bits >>> 23) & 0xff;
|
|
36
|
+
let mant = bits & 0x7fffff;
|
|
37
|
+
let half: number;
|
|
38
|
+
if (exp === 0xff) {
|
|
39
|
+
half = (sign << 15) | 0x7c00 | (mant ? 0x200 : 0);
|
|
40
|
+
} else if (exp === 0) {
|
|
41
|
+
half = sign << 15;
|
|
42
|
+
} else {
|
|
43
|
+
const e = exp - 127 + 15;
|
|
44
|
+
if (e >= 0x1f) {
|
|
45
|
+
half = (sign << 15) | 0x7c00;
|
|
46
|
+
} else if (e <= 0) {
|
|
47
|
+
if (e < -10) {
|
|
48
|
+
half = sign << 15;
|
|
49
|
+
} else {
|
|
50
|
+
mant = (mant | 0x800000) >> (1 - e);
|
|
51
|
+
if (mant & 0x1000) mant += 0x2000;
|
|
52
|
+
half = (sign << 15) | (mant >> 13);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (mant & 0x1000) {
|
|
56
|
+
mant += 0x2000;
|
|
57
|
+
if (mant & 0x800000) {
|
|
58
|
+
mant = 0;
|
|
59
|
+
half = (sign << 15) | ((e + 1) << 10);
|
|
60
|
+
} else {
|
|
61
|
+
half = (sign << 15) | (e << 10) | (mant >> 13);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
half = (sign << 15) | (e << 10) | (mant >> 13);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
view.setUint16(i * 2, half & 0xffff, true);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|