@sarmal/core 0.29.1 → 0.31.0

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/dist/index.cjs CHANGED
@@ -385,6 +385,78 @@ function hexToRgb(hex) {
385
385
  const n = parseInt(hex.slice(1), 16);
386
386
  return { r: n >> 16, g: n >> 8 & 255, b: n & 255 };
387
387
  }
388
+ var HEX_3_RE = /^#([0-9a-fA-F]{3})$/;
389
+ var HEX_6_RE = /^#([0-9a-fA-F]{6})$/;
390
+ var HEX_8_RE = /^#([0-9a-fA-F]{8})$/;
391
+ var RGB_RE = /^rgba?\(\s*(-?\d{1,3})\s*,\s*(-?\d{1,3})\s*,\s*(-?\d{1,3})(?:\s*,\s*[\d.]+)?\s*\)$/i;
392
+ function parseColorToRgb(s) {
393
+ const trimmed = s.trim();
394
+ const m3 = HEX_3_RE.exec(trimmed);
395
+ if (m3) {
396
+ const [r, g, b] = m3[1];
397
+ return hexToRgb(`#${r}${r}${g}${g}${b}${b}`);
398
+ }
399
+ const m6 = HEX_6_RE.exec(trimmed);
400
+ if (m6) {
401
+ return hexToRgb(trimmed);
402
+ }
403
+ const m8 = HEX_8_RE.exec(trimmed);
404
+ if (m8) {
405
+ return hexToRgb(`#${trimmed.slice(1, 7)}`);
406
+ }
407
+ const mRgb = RGB_RE.exec(trimmed);
408
+ if (mRgb) {
409
+ return {
410
+ r: Math.max(0, Math.min(255, parseInt(mRgb[1], 10))),
411
+ g: Math.max(0, Math.min(255, parseInt(mRgb[2], 10))),
412
+ b: Math.max(0, Math.min(255, parseInt(mRgb[3], 10)))
413
+ };
414
+ }
415
+ return null;
416
+ }
417
+ var OKLCH_RE = /^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*[\d.]+)?\s*\)$/i;
418
+ function parseOklchToOklab(s) {
419
+ const m = OKLCH_RE.exec(s.trim());
420
+ if (!m) {
421
+ return null;
422
+ }
423
+ const L = parseFloat(m[1]);
424
+ const C = parseFloat(m[2]);
425
+ const H = parseFloat(m[3]);
426
+ if (Number.isNaN(L) || Number.isNaN(C) || Number.isNaN(H)) {
427
+ return null;
428
+ }
429
+ const clampedL = Math.max(0, Math.min(1, L));
430
+ const clampedC = Math.max(0, Math.min(0.4, C));
431
+ const H_rad = H * (Math.PI / 180);
432
+ return {
433
+ L: clampedL,
434
+ a: clampedC * Math.cos(H_rad),
435
+ b: clampedC * Math.sin(H_rad)
436
+ };
437
+ }
438
+ function parseColorToOklab(s) {
439
+ const oklab = parseOklchToOklab(s);
440
+ if (oklab !== null) {
441
+ return oklab;
442
+ }
443
+ const rgb = parseColorToRgb(s);
444
+ if (rgb === null) {
445
+ return null;
446
+ }
447
+ return rgbToOklab(rgb);
448
+ }
449
+ function colorToRgb(color) {
450
+ const rgb = parseColorToRgb(color);
451
+ if (rgb !== null) {
452
+ return rgb;
453
+ }
454
+ const lab = parseOklchToOklab(color);
455
+ if (lab !== null) {
456
+ return oklabToRgb(lab);
457
+ }
458
+ throw new Error(`[sarmal] unrecognized color "${color}"`);
459
+ }
388
460
  function srgbByteToLinear(c) {
389
461
  const n = c / 255;
390
462
  return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
@@ -422,29 +494,27 @@ var lerpOklab = (a, b, t) => {
422
494
  if (t >= 1) {
423
495
  return b;
424
496
  }
425
- const la = rgbToOklab(a), lb = rgbToOklab(b);
426
- return oklabToRgb({
427
- L: la.L + (lb.L - la.L) * t,
428
- a: la.a + (lb.a - la.a) * t,
429
- b: la.b + (lb.b - la.b) * t
430
- });
497
+ return {
498
+ L: a.L + (b.L - a.L) * t,
499
+ a: a.a + (b.a - a.a) * t,
500
+ b: a.b + (b.b - a.b) * t
501
+ };
431
502
  };
432
503
  function getPaletteColor(palette, position, timeOffset = 0) {
433
504
  if (palette.length === 0) {
434
- return { r: 255, g: 255, b: 255 };
505
+ return { L: 1, a: 0, b: 0 };
435
506
  }
436
507
  if (palette.length === 1) {
437
- return hexToRgb(palette[0]);
508
+ return palette[0];
438
509
  }
439
510
  const cyclePos = ((position + timeOffset) % 1 + 1) % 1;
440
511
  const scaled = cyclePos * palette.length;
441
512
  const idx = Math.floor(scaled);
442
513
  const t = scaled - idx;
443
- const c1 = hexToRgb(palette[idx % palette.length]);
444
- const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
514
+ const c1 = palette[idx % palette.length];
515
+ const c2 = palette[(idx + 1) % palette.length];
445
516
  return lerpOklab(c1, c2, t);
446
517
  }
447
- var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
448
518
  var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
449
519
  var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
450
520
  "trailColor",
@@ -477,9 +547,9 @@ function validateRenderOptions(partial) {
477
547
  }
478
548
  function assertTrailColor(value) {
479
549
  if (typeof value === "string") {
480
- if (!HEX_COLOR_RE.test(value)) {
550
+ if (parseColorToOklab(value) === null) {
481
551
  throw new TypeError(
482
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`
552
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()), got "${value}"`
483
553
  );
484
554
  }
485
555
  return;
@@ -492,25 +562,25 @@ function assertTrailColor(value) {
492
562
  }
493
563
  for (let i = 0; i < value.length; i++) {
494
564
  const entry = value[i];
495
- if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
565
+ if (typeof entry !== "string" || parseColorToOklab(entry) === null) {
496
566
  throw new TypeError(
497
- `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`
567
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()), got ${JSON.stringify(entry)}`
498
568
  );
499
569
  }
500
570
  }
501
571
  return;
502
572
  }
503
573
  throw new TypeError(
504
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`
574
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or an array of color strings, got ${JSON.stringify(value)}`
505
575
  );
506
576
  }
507
577
  function assertHeadColor(value) {
508
578
  if (value === null) {
509
579
  return;
510
580
  }
511
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
581
+ if (typeof value !== "string" || parseColorToOklab(value) === null) {
512
582
  throw new TypeError(
513
- `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`
583
+ `[sarmal] setRenderOptions: headColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or null, got ${JSON.stringify(value)}`
514
584
  );
515
585
  }
516
586
  }
@@ -518,9 +588,9 @@ function assertSkeletonColor(value) {
518
588
  if (value === "transparent") {
519
589
  return;
520
590
  }
521
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
591
+ if (typeof value !== "string" || parseColorToOklab(value) === null) {
522
592
  throw new TypeError(
523
- `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`
593
+ `[sarmal] setRenderOptions: skeletonColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or "transparent", got ${JSON.stringify(value)}`
524
594
  );
525
595
  }
526
596
  }
@@ -555,7 +625,7 @@ function resolveHeadColor(trailColor, trailStyle) {
555
625
  }
556
626
  const palette = resolveTrailPalette(trailColor);
557
627
  const last = palette[palette.length - 1];
558
- const { r, g, b } = hexToRgb(last);
628
+ const { r, g, b } = colorToRgb(last);
559
629
  return `rgb(${r},${g},${b})`;
560
630
  }
561
631
  function warnIfTrailColorMismatch(trailColor, trailStyle) {
@@ -575,9 +645,9 @@ function warnIfTrailColorMismatch(trailColor, trailStyle) {
575
645
  // src/renderer.ts
576
646
  var getHeadDotRadius = (w, h) => Math.max(1, 3 * Math.sqrt(Math.min(w, h) / 160));
577
647
  var WHITE_HEX = "#ffffff";
578
- function hexToRgbComponents(hex) {
579
- const n = parseInt(hex.slice(1), 16);
580
- return `${n >> 16},${n >> 8 & 255},${n & 255}`;
648
+ function colorToRgbComponents(color) {
649
+ const { r, g, b } = colorToRgb(color);
650
+ return `${r},${g},${b}`;
581
651
  }
582
652
  function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
583
653
  target.style.width = `${logicalWidth}px`;
@@ -597,8 +667,9 @@ function createRenderer(options) {
597
667
  let skeletonColor = options.skeletonColor ?? WHITE_HEX;
598
668
  let userHeadColor = options.headColor ?? null;
599
669
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
600
- let trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
670
+ let trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
601
671
  let trailPalette = resolveTrailPalette(trailColor);
672
+ let trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
602
673
  warnIfTrailColorMismatch(trailColor, trailStyle);
603
674
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
604
675
  function setupCanvas() {
@@ -642,7 +713,7 @@ function createRenderer(options) {
642
713
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
643
714
  const skeletonCtx = skeletonCanvas.getContext("2d");
644
715
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
645
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
716
+ skeletonCtx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
646
717
  skeletonCtx.lineWidth = 1.5;
647
718
  skeletonCtx.beginPath();
648
719
  const first = skeleton[0];
@@ -657,7 +728,7 @@ function createRenderer(options) {
657
728
  if (pts.length < 2) {
658
729
  return;
659
730
  }
660
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
731
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${opacity})`;
661
732
  ctx.lineWidth = 1.5;
662
733
  ctx.beginPath();
663
734
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -678,7 +749,7 @@ function createRenderer(options) {
678
749
  if (skeleton.length < 2) {
679
750
  return;
680
751
  }
681
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
752
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
682
753
  ctx.lineWidth = 1.5;
683
754
  ctx.beginPath();
684
755
  const first = skeleton[0];
@@ -710,8 +781,8 @@ function createRenderer(options) {
710
781
  ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
711
782
  } else {
712
783
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
713
- const color = getPaletteColor(trailPalette, progress, timeOffset);
714
- ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
784
+ const { r, g, b } = oklabToRgb(getPaletteColor(trailPaletteOklab, progress, timeOffset));
785
+ ctx.fillStyle = `rgba(${r},${g},${b},${opacity})`;
715
786
  }
716
787
  ctx.beginPath();
717
788
  ctx.moveTo(l0x, l0y);
@@ -755,7 +826,7 @@ function createRenderer(options) {
755
826
  morphReject = null;
756
827
  morphAlpha = 0;
757
828
  skeleton = engine.getSarmalSkeleton();
758
- if (!engine.isLiveSkeleton) {
829
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
759
830
  buildSkeletonCanvas();
760
831
  }
761
832
  }
@@ -781,7 +852,7 @@ function createRenderer(options) {
781
852
  }
782
853
  skeleton = engine.getSarmalSkeleton();
783
854
  calculateBoundaries();
784
- if (!engine.isLiveSkeleton) {
855
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
785
856
  buildSkeletonCanvas();
786
857
  }
787
858
  if (options.initialPhase !== void 0) {
@@ -843,8 +914,9 @@ function createRenderer(options) {
843
914
  validateRenderOptions(partial);
844
915
  if (partial.trailColor !== void 0) {
845
916
  trailColor = partial.trailColor;
846
- trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
917
+ trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
847
918
  trailPalette = resolveTrailPalette(trailColor);
919
+ trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
848
920
  }
849
921
  if (partial.skeletonColor !== void 0) {
850
922
  skeletonColor = partial.skeletonColor;
@@ -927,6 +999,10 @@ function sampleCurveSkeleton(curveDef) {
927
999
  function el(tag) {
928
1000
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
929
1001
  }
1002
+ function colorToRgbAttr(color) {
1003
+ const { r, g, b } = colorToRgb(color);
1004
+ return `rgb(${r},${g},${b})`;
1005
+ }
930
1006
  function createSVGRenderer(options) {
931
1007
  const { container, engine } = options;
932
1008
  const poolSize = engine.trailLength;
@@ -941,8 +1017,9 @@ function createSVGRenderer(options) {
941
1017
  let userHeadColor = options.headColor ?? null;
942
1018
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
943
1019
  let headRadius;
944
- let trailSolid = resolveTrailMainColor(trailColor);
1020
+ let trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
945
1021
  let trailPalette = resolveTrailPalette(trailColor);
1022
+ let trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
946
1023
  const ariaLabel = options.ariaLabel ?? "Loading";
947
1024
  warnIfTrailColorMismatch(trailColor, trailStyle);
948
1025
  const viewSize = 100;
@@ -965,7 +1042,10 @@ function createSVGRenderer(options) {
965
1042
  const skeletonPath = el("path");
966
1043
  skeletonPath.setAttribute("data-sarmal-role", "skeleton");
967
1044
  skeletonPath.setAttribute("fill", "none");
968
- skeletonPath.setAttribute("stroke", skeletonColor);
1045
+ skeletonPath.setAttribute(
1046
+ "stroke",
1047
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1048
+ );
969
1049
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
970
1050
  skeletonPath.setAttribute("stroke-width", svgSkeletonStrokeWidth);
971
1051
  if (skeletonColor === "transparent") {
@@ -974,13 +1054,19 @@ function createSVGRenderer(options) {
974
1054
  group.appendChild(skeletonPath);
975
1055
  const skeletonPathA = el("path");
976
1056
  skeletonPathA.setAttribute("fill", "none");
977
- skeletonPathA.setAttribute("stroke", skeletonColor);
1057
+ skeletonPathA.setAttribute(
1058
+ "stroke",
1059
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1060
+ );
978
1061
  skeletonPathA.setAttribute("stroke-width", svgSkeletonStrokeWidth);
979
1062
  skeletonPathA.setAttribute("visibility", "hidden");
980
1063
  group.appendChild(skeletonPathA);
981
1064
  const skeletonPathB = el("path");
982
1065
  skeletonPathB.setAttribute("fill", "none");
983
- skeletonPathB.setAttribute("stroke", skeletonColor);
1066
+ skeletonPathB.setAttribute(
1067
+ "stroke",
1068
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1069
+ );
984
1070
  skeletonPathB.setAttribute("stroke-width", svgSkeletonStrokeWidth);
985
1071
  skeletonPathB.setAttribute("visibility", "hidden");
986
1072
  group.appendChild(skeletonPathB);
@@ -995,7 +1081,7 @@ function createSVGRenderer(options) {
995
1081
  }
996
1082
  const headCircle = el("circle");
997
1083
  headCircle.setAttribute("data-sarmal-role", "head");
998
- headCircle.setAttribute("fill", headColor);
1084
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
999
1085
  headCircle.setAttribute("r", String(headRadius));
1000
1086
  group.appendChild(headCircle);
1001
1087
  container.appendChild(group);
@@ -1048,7 +1134,7 @@ function createSVGRenderer(options) {
1048
1134
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
1049
1135
  if (trailStyle !== "default") {
1050
1136
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
1051
- const { r, g, b } = getPaletteColor(trailPalette, progress, timeOffset);
1137
+ const { r, g, b } = oklabToRgb(getPaletteColor(trailPaletteOklab, progress, timeOffset));
1052
1138
  trailPaths[i].setAttribute("fill", `rgb(${r},${g},${b})`);
1053
1139
  }
1054
1140
  }
@@ -1198,8 +1284,9 @@ function createSVGRenderer(options) {
1198
1284
  const prevTrailStyle = trailStyle;
1199
1285
  if (partial.trailColor !== void 0) {
1200
1286
  trailColor = partial.trailColor;
1201
- trailSolid = resolveTrailMainColor(trailColor);
1287
+ trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
1202
1288
  trailPalette = resolveTrailPalette(trailColor);
1289
+ trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
1203
1290
  if (trailStyle === "default") {
1204
1291
  for (const p of trailPaths) {
1205
1292
  p.setAttribute("fill", trailSolid);
@@ -1211,10 +1298,10 @@ function createSVGRenderer(options) {
1211
1298
  if (skeletonColor === "transparent") {
1212
1299
  skeletonPath.setAttribute("visibility", "hidden");
1213
1300
  } else {
1214
- skeletonPath.setAttribute("stroke", skeletonColor);
1301
+ skeletonPath.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1215
1302
  skeletonPath.removeAttribute("visibility");
1216
- skeletonPathA.setAttribute("stroke", skeletonColor);
1217
- skeletonPathB.setAttribute("stroke", skeletonColor);
1303
+ skeletonPathA.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1304
+ skeletonPathB.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1218
1305
  }
1219
1306
  }
1220
1307
  if (partial.trailStyle !== void 0) {
@@ -1237,7 +1324,7 @@ function createSVGRenderer(options) {
1237
1324
  } else {
1238
1325
  headColor = userHeadColor;
1239
1326
  }
1240
- headCircle.setAttribute("fill", headColor);
1327
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
1241
1328
  if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1242
1329
  warnIfTrailColorMismatch(trailColor, trailStyle);
1243
1330
  }