@forgeax/engine-math 0.1.23 → 0.1.25

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/README.md CHANGED
@@ -6,7 +6,7 @@ Pure-function SoA-friendly Vec / Mat / Quat / Color / Euler math library for for
6
6
  ## 30-second self-introduction
7
7
 
8
8
  - **ABI**: `Float32Array & { readonly __<dim>: void }` 七件套(`Vec2 / Vec3 / Vec4 / Quat / Mat3 / Mat4 / Color`)+ `Euler` plain-object;branded 维度互斥(编译期)+ TypedArray 即时上传 GPU(运行期)。
9
- - **Surface**: 18 namespace × 202 函数(vec2:20 / vec3:21 / vec4:18 / mat3:10 / mat4:33 / quat:23 / euler:6 / color:6 / easing:4 / noise:1 / frustum:4 / ray:9 / ray2:11 / box2:13 / box3:7 / circle2:10 / sphere:5 / f32-to-f16-bytes:1);单 entry `import { Vec3, vec3, ... } from '@forgeax/engine-math'`。
9
+ - **Surface**: 18 namespace × 203 函数(vec2:20 / vec3:21 / vec4:18 / mat3:10 / mat4:33 / quat:23 / euler:6 / color:7 / easing:4 / noise:1 / frustum:4 / ray:9 / ray2:11 / box2:13 / box3:7 / circle2:10 / sphere:5 / f32-to-f16-bytes:1);单 entry `import { Vec3, vec3, ... } from '@forgeax/engine-math'`。
10
10
  - **Style**: gl-matrix / wgpu-matrix 风纯函数 namespace + out-param-first(`func(out, ...args)`);零分配;aliasing-safe(`add(v, v, v)` 合法)。
11
11
  - **Errors**: 全库静默回退(不抛错、不返回 `null`、不返回 `Result`、不 `console.warn`);退化语义靠 JSDoc `@degrade` + 本 README §退化策略表 + `*.test-d.ts` 三层共担。
12
12
 
@@ -33,7 +33,7 @@ vec3.normalize(v, vec3.create(0, 0, 0)); // v = (0, 0, 0)
33
33
 
34
34
  > 上述代码覆盖 v0.1 PBR 渲染管线 90% 的典型 transform / projection / shader-uniform 准备路径——LLM 一次扫读即可拿到 "概念→签名→典型流程" 完整闭包(charter 命题 1)。
35
35
 
36
- ## quick-ref:18 namespace × 202 函数
36
+ ## quick-ref:18 namespace × 203 函数
37
37
 
38
38
  每 namespace 内函数顺序粗略按 "构造 → 比较 → 算术 → 几何 → 高级" 分组。完整签名以 `.d.ts` JSDoc 为 SSOT。
39
39
 
@@ -46,7 +46,7 @@ vec3.normalize(v, vec3.create(0, 0, 0)); // v = (0, 0, 0)
46
46
  | **`mat4`** (33) | `create / clone / identity / equals / multiply / transpose / invert / scale / translate / rotate / lookAt / compose / decompose / fromQuat / fromTranslation / fromScaling / fromRotation / perspective / perspectiveNO / perspectiveReverseZ / orthographic / orthographicNO / orthographicReverseZ / transformVec3 / transformPoint / transformDirection / getTranslation / getForward / getUp / getRight / unproject / projectPoint / computeViewProj` |
47
47
  | **`quat`** (23) | `create / clone / identity / fromAxisAngle / fromEuler / fromRotationMatrix / fromLookAt / fromUnitVectors / multiply / rotateAxis / slerp / nlerp / invert / conjugate / dot / length / lengthSq / normalize / transformVec3 / eulerY / right / up / forward` |
48
48
  | **`euler`** (6) | `create / clone / set / toQuat / fromQuat / fromRotationMatrix` |
49
- | **`color`** (6) | `create / clone / srgbToLinear / linearToSrgb / fromHex / toHex` |
49
+ | **`color`** (7) | `create / clone / srgbToLinear / linearToSrgb / fromHex / fromCss / toHex` |
50
50
  | **`easing`** (4) | `cubicInOut / smoothstep / smootherstep / elasticInOut` — scalar time-remaps (clamp t to [0,1]; Bevy `EaseFunction`; slow-in/slow-out with elastic overshoot). Growable home for further ease variants |
51
51
  | **`noise`** (1) | `perlin1d` — 1D Perlin noise returning [-1,1]. Deterministic, smooth (nearby inputs → nearby outputs); canonical permutation table matching Bevy's `2d_screen_shake`. Growable home for 2D/3D/Simplex variants |
52
52
  | **`frustum`** (4) | `create / fromViewProjection / intersectsBox / intersectsSphere` |
@@ -238,7 +238,8 @@ const projWebGL = mat4.perspectiveNO(mat4.create(), Math.PI / 4, 16 / 9, 0.1, 10
238
238
  const projReverseZ = mat4.perspectiveReverseZ(mat4.create(), Math.PI / 4, 16 / 9, 0.1, 100);
239
239
  ```
240
240
 
241
- `orthographic / orthographicNO / orthographicReverseZ` 三档同形(`orthographicReverseZ` 为本库自创扩展,详见末尾对照表脚注)。
241
+ `orthographic / orthographicNO / orthographicReverseZ` 三档同形,位置参数统一为
242
+ `(out, left, right, top, bottom, near, far)`(`orthographicReverseZ` 为本库自创扩展,详见末尾对照表脚注)。
242
243
 
243
244
  ## 非目标(Non-goals)
244
245
 
@@ -255,7 +256,7 @@ const projReverseZ = mat4.perspectiveReverseZ(mat4.create(), Math.PI / 4, 16 / 9
255
256
 
256
257
  | 版本 | 范围 |
257
258
  |:--|:--|
258
- | **v0.0 (本闭环)** | 18 namespace × 202 函数(含 mat4 / quat 反向 surface 跨类型 transform 4 函数、2D bounds/raycast/cast,以及 easing 标量缓动 + noise 1D Perlin);branded ABI;3 档 NDC 投影;M1-M6 全 19 AC pass |
259
+ | **v0.0 (本闭环)** | 18 namespace × 203 函数(含 mat4 / quat 反向 surface 跨类型 transform 4 函数、2D bounds/raycast/cast,以及 easing 标量缓动 + noise 1D Perlin);branded ABI;3 档 NDC 投影;M1-M6 全 19 AC pass |
259
260
  | v0.1 (PBR 闭环) | `mat3.fromMat4Scale` 法线矩阵优化路径;`Color.linearToSRGB` GPU 上传专用变体;高级 PBR 工具 |
260
261
  | v0.2 (动画闭环) | `quat.squad` Catmull-Rom 球面插值;`euler.lookAt` 自然映射;动画系统消费方向定 |
261
262
  | v1.0 (稳定 ABI) | 不再 breaking ABI;coverage 阈值不退步;`@forgeax/engine-math` 正式 `npm publish` |
package/dist/color.d.ts CHANGED
@@ -39,12 +39,21 @@ export declare function linearToSrgb(out: Color, c: ColorLike): Color;
39
39
  *
40
40
  * @example
41
41
  * ```ts
42
- * color.fromHex(out, '#ff8000'); // → (1, 0.502, 0, 1)
43
- * color.fromHex(out, '#ff800080'); // → (1, 0.502, 0, 0.502)
42
+ * color.fromHex(out, '#ff8000'); // → (1, ~0.216, 0, 1)
43
+ * color.fromHex(out, '#ff800080'); // → (1, ~0.216, 0, 0.502)
44
44
  * color.fromHex(out, '#fff'); // → (0, 0, 0, 1) short form unsupported → silent fall-back
45
45
  * ```
46
46
  */
47
47
  export declare function fromHex(out: Color, hex: string): Color;
48
+ /**
49
+ * Write a Color from a deterministic CSS color string.
50
+ *
51
+ * Supports full and short hex, common named colors, rgb()/rgba() channel
52
+ * numbers or percentages, and hsl()/hsla() with an optional alpha. RGB
53
+ * channels are decoded to linear-sRGB; alpha is passed through unchanged.
54
+ * Unsupported or malformed input silently falls back to opaque black.
55
+ */
56
+ export declare function fromCss(out: Color, css: string): Color;
48
57
  /**
49
58
  * Serialize as hex string: alpha=1 → `#rrggbb`; alpha<1 → `#rrggbbaa`.
50
59
  *
@@ -55,7 +64,7 @@ export declare function fromHex(out: Color, hex: string): Color;
55
64
  *
56
65
  * @example
57
66
  * ```ts
58
- * color.toHex(color.create(2, -0.1, 0.5, 1)); // → '#ff0080' (HDR/negative clamp)
67
+ * color.toHex(color.create(2, -0.1, 0.5, 1)); // → '#ff00bc' (linear HDR/negative clamp)
59
68
  * color.toHex(color.create(1, 0, 0, 0.5)); // → '#ff000080'
60
69
  * color.toHex(color.create(NaN, 0, 0, NaN)); // → '#000000' (NaN→'00', alpha=NaN takes short form)
61
70
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhD,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAIjC,mEAAmE;AACnE,wBAAgB,MAAM,CAAC,CAAC,SAAI,EAAE,CAAC,SAAI,EAAE,CAAC,SAAI,EAAE,CAAC,SAAI,GAAG,KAAK,CAExD;AAED,iCAAiC;AACjC,wBAAgB,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,KAAK,CAEzC;AAgCD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,KAAK,CAM5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,KAAK,CAM5D;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CActD;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAU1C"}
1
+ {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../src/color.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEhD,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAIjC,mEAAmE;AACnE,wBAAgB,MAAM,CAAC,CAAC,SAAI,EAAE,CAAC,SAAI,EAAE,CAAC,SAAI,EAAE,CAAC,SAAI,GAAG,KAAK,CAExD;AAED,iCAAiC;AACjC,wBAAgB,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,KAAK,CAEzC;AAgCD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,KAAK,CAM5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,KAAK,CAM5D;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CActD;AA8ID;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAuDtD;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAU1C"}
package/dist/index.mjs CHANGED
@@ -487,6 +487,7 @@ var color_exports = {};
487
487
  __export(color_exports, {
488
488
  clone: () => clone,
489
489
  create: () => create4,
490
+ fromCss: () => fromCss,
490
491
  fromHex: () => fromHex,
491
492
  linearToSrgb: () => linearToSrgb,
492
493
  srgbToLinear: () => srgbToLinear,
@@ -534,15 +535,174 @@ function fromHex(out, hex) {
534
535
  out[3] = 1;
535
536
  return out;
536
537
  }
537
- out[0] = Number.parseInt(match[1], 16) / 255;
538
- out[1] = Number.parseInt(match[2], 16) / 255;
539
- out[2] = Number.parseInt(match[3], 16) / 255;
538
+ out[0] = srgbChannelToLinear(Number.parseInt(match[1], 16) / 255);
539
+ out[1] = srgbChannelToLinear(Number.parseInt(match[2], 16) / 255);
540
+ out[2] = srgbChannelToLinear(Number.parseInt(match[3], 16) / 255);
540
541
  out[3] = match[4] !== void 0 ? Number.parseInt(match[4], 16) / 255 : 1;
541
542
  return out;
542
543
  }
543
- function toHexByte(v) {
544
+ var CSS_HEX_PATTERN = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
545
+ var CSS_RGB_PATTERN = /^rgba?\(([^()]*)\)$/i;
546
+ var CSS_HSL_PATTERN = /^hsla?\(([^()]*)\)$/i;
547
+ var CSS_DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
548
+ var CSS_NAMED_COLORS = {
549
+ black: "#000000",
550
+ blue: "#0000ff",
551
+ cyan: "#00ffff",
552
+ fuchsia: "#ff00ff",
553
+ gray: "#808080",
554
+ green: "#008000",
555
+ grey: "#808080",
556
+ lime: "#00ff00",
557
+ magenta: "#ff00ff",
558
+ maroon: "#800000",
559
+ navy: "#000080",
560
+ olive: "#808000",
561
+ orange: "#ffa500",
562
+ purple: "#800080",
563
+ red: "#ff0000",
564
+ silver: "#c0c0c0",
565
+ teal: "#008080",
566
+ transparent: "#00000000",
567
+ white: "#ffffff",
568
+ yellow: "#ffff00"
569
+ };
570
+ function clamp01(v) {
571
+ return Math.min(1, Math.max(0, v));
572
+ }
573
+ function parseCssChannel(token) {
574
+ const trimmed = token.trim();
575
+ if (trimmed.endsWith("%")) {
576
+ const raw = trimmed.slice(0, -1).trim();
577
+ if (!CSS_DECIMAL_PATTERN.test(raw)) return void 0;
578
+ const value2 = Number(raw);
579
+ return Number.isFinite(value2) ? clamp01(value2 / 100) : void 0;
580
+ }
581
+ if (!CSS_DECIMAL_PATTERN.test(trimmed)) return void 0;
582
+ const value = Number(trimmed);
583
+ return Number.isFinite(value) ? clamp01(value / 255) : void 0;
584
+ }
585
+ function parseCssAlpha(token) {
586
+ const trimmed = token.trim();
587
+ if (trimmed.endsWith("%")) {
588
+ const raw = trimmed.slice(0, -1).trim();
589
+ if (!CSS_DECIMAL_PATTERN.test(raw)) return void 0;
590
+ const value2 = Number(raw);
591
+ return Number.isFinite(value2) ? clamp01(value2 / 100) : void 0;
592
+ }
593
+ if (!CSS_DECIMAL_PATTERN.test(trimmed)) return void 0;
594
+ const value = Number(trimmed);
595
+ return Number.isFinite(value) ? clamp01(value) : void 0;
596
+ }
597
+ function parseCssHue(token) {
598
+ const match = /^([+-]?(?:\d+\.?\d*|\.\d+))(deg|grad|rad|turn)?$/i.exec(token.trim());
599
+ if (match === null) return void 0;
600
+ const raw = Number(match[1]);
601
+ if (!Number.isFinite(raw)) return void 0;
602
+ const unit = (match[2] ?? "deg").toLowerCase();
603
+ const degrees = unit === "grad" ? raw * 0.9 : unit === "rad" ? raw * 180 / Math.PI : unit === "turn" ? raw * 360 : raw;
604
+ return (degrees % 360 + 360) % 360;
605
+ }
606
+ function parseCssPercentage(token) {
607
+ const trimmed = token.trim();
608
+ if (!trimmed.endsWith("%")) return void 0;
609
+ const raw = trimmed.slice(0, -1).trim();
610
+ if (!CSS_DECIMAL_PATTERN.test(raw)) return void 0;
611
+ const value = Number(raw);
612
+ return Number.isFinite(value) ? clamp01(value / 100) : void 0;
613
+ }
614
+ function splitCssColorArgs(body) {
615
+ const source = body.trim();
616
+ if (source.length === 0) return void 0;
617
+ if (source.includes(",")) {
618
+ if (source.includes("/")) return void 0;
619
+ const parts = source.split(",").map((part) => part.trim());
620
+ if (parts.length !== 3 && parts.length !== 4) return void 0;
621
+ if (parts.some((part) => part.length === 0)) return void 0;
622
+ return {
623
+ channels: [parts[0], parts[1], parts[2]],
624
+ ...parts.length === 4 ? { alpha: parts[3] } : {}
625
+ };
626
+ }
627
+ const tokens = source.replaceAll("/", " / ").split(/\s+/);
628
+ const slash = tokens.indexOf("/");
629
+ if (slash === -1) {
630
+ if (tokens.length !== 3) return void 0;
631
+ return { channels: [tokens[0], tokens[1], tokens[2]] };
632
+ }
633
+ if (slash !== 3 || tokens.length !== 5 || tokens[4] === void 0) return void 0;
634
+ return {
635
+ channels: [tokens[0], tokens[1], tokens[2]],
636
+ alpha: tokens[4]
637
+ };
638
+ }
639
+ function hslToSrgb(hue, saturation, lightness) {
640
+ const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
641
+ const h = hue / 60;
642
+ const x = chroma * (1 - Math.abs(h % 2 - 1));
643
+ const [r1, g1, b1] = h < 1 ? [chroma, x, 0] : h < 2 ? [x, chroma, 0] : h < 3 ? [0, chroma, x] : h < 4 ? [0, x, chroma] : h < 5 ? [x, 0, chroma] : [chroma, 0, x];
644
+ const m = lightness - chroma / 2;
645
+ return [r1 + m, g1 + m, b1 + m];
646
+ }
647
+ function cssFallback(out) {
648
+ out[0] = 0;
649
+ out[1] = 0;
650
+ out[2] = 0;
651
+ out[3] = 1;
652
+ return out;
653
+ }
654
+ function fromCss(out, css) {
655
+ if (typeof css !== "string") return cssFallback(out);
656
+ const source = css.trim().toLowerCase();
657
+ const named = CSS_NAMED_COLORS[source];
658
+ if (named !== void 0) return fromHex(out, named);
659
+ const fullHex = CSS_HEX_PATTERN.exec(source);
660
+ if (fullHex !== null) {
661
+ const digits = fullHex[1];
662
+ if (digits.length === 3 || digits.length === 4) {
663
+ const expanded = [...digits].map((channel) => `${channel}${channel}`).join("");
664
+ return fromHex(out, `#${expanded}`);
665
+ }
666
+ return fromHex(out, source);
667
+ }
668
+ const rgb = CSS_RGB_PATTERN.exec(source);
669
+ if (rgb !== null) {
670
+ const args = splitCssColorArgs(rgb[1]);
671
+ if (args === void 0) return cssFallback(out);
672
+ const r = parseCssChannel(args.channels[0]);
673
+ const g = parseCssChannel(args.channels[1]);
674
+ const b = parseCssChannel(args.channels[2]);
675
+ const alpha = args.alpha === void 0 ? 1 : parseCssAlpha(args.alpha);
676
+ if (r === void 0 || g === void 0 || b === void 0 || alpha === void 0)
677
+ return cssFallback(out);
678
+ out[0] = srgbChannelToLinear(r);
679
+ out[1] = srgbChannelToLinear(g);
680
+ out[2] = srgbChannelToLinear(b);
681
+ out[3] = alpha;
682
+ return out;
683
+ }
684
+ const hsl = CSS_HSL_PATTERN.exec(source);
685
+ if (hsl !== null) {
686
+ const args = splitCssColorArgs(hsl[1]);
687
+ if (args === void 0) return cssFallback(out);
688
+ const hue = parseCssHue(args.channels[0]);
689
+ const saturation = parseCssPercentage(args.channels[1]);
690
+ const lightness = parseCssPercentage(args.channels[2]);
691
+ const alpha = args.alpha === void 0 ? 1 : parseCssAlpha(args.alpha);
692
+ if (hue === void 0 || saturation === void 0 || lightness === void 0 || alpha === void 0)
693
+ return cssFallback(out);
694
+ const [r, g, b] = hslToSrgb(hue, saturation, lightness);
695
+ out[0] = srgbChannelToLinear(r);
696
+ out[1] = srgbChannelToLinear(g);
697
+ out[2] = srgbChannelToLinear(b);
698
+ out[3] = alpha;
699
+ return out;
700
+ }
701
+ return cssFallback(out);
702
+ }
703
+ function toHexByte(v, encodeLinear = true) {
544
704
  if (Number.isNaN(v)) return "00";
545
- const clamped = Math.max(0, Math.min(1, v));
705
+ const clamped = Math.max(0, Math.min(1, encodeLinear ? linearChannelToSrgb(v) : v));
546
706
  const byte = Math.round(clamped * 255);
547
707
  return byte.toString(16).padStart(2, "0");
548
708
  }
@@ -554,7 +714,7 @@ function toHex(c) {
554
714
  if (aValue >= 1 || Number.isNaN(aValue)) {
555
715
  return `#${r}${g}${b}`;
556
716
  }
557
- const a = toHexByte(aValue);
717
+ const a = toHexByte(aValue, false);
558
718
  return `#${r}${g}${b}${a}`;
559
719
  }
560
720
 
@@ -2467,7 +2627,7 @@ function perspectiveReverseZ(out, fovYRadians, aspect, near, far) {
2467
2627
  }
2468
2628
  return out;
2469
2629
  }
2470
- function orthographic(out, left, right2, bottom, top, near, far) {
2630
+ function orthographic(out, left, right2, top, bottom, near, far) {
2471
2631
  const lr = 1 / (left - right2);
2472
2632
  const bt = 1 / (bottom - top);
2473
2633
  const nf = 1 / (near - far);
@@ -2489,7 +2649,7 @@ function orthographic(out, left, right2, bottom, top, near, far) {
2489
2649
  out[15] = 1;
2490
2650
  return out;
2491
2651
  }
2492
- function orthographicNO(out, left, right2, bottom, top, near, far) {
2652
+ function orthographicNO(out, left, right2, top, bottom, near, far) {
2493
2653
  const lr = 1 / (left - right2);
2494
2654
  const bt = 1 / (bottom - top);
2495
2655
  const nf = 1 / (near - far);
@@ -2511,7 +2671,7 @@ function orthographicNO(out, left, right2, bottom, top, near, far) {
2511
2671
  out[15] = 1;
2512
2672
  return out;
2513
2673
  }
2514
- function orthographicReverseZ(out, left, right2, bottom, top, near, far) {
2674
+ function orthographicReverseZ(out, left, right2, top, bottom, near, far) {
2515
2675
  const lr = 1 / (left - right2);
2516
2676
  const bt = 1 / (bottom - top);
2517
2677
  const fn = 1 / (far - near);