@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.
@@ -377,6 +377,78 @@ function hexToRgb(hex) {
377
377
  const n = parseInt(hex.slice(1), 16);
378
378
  return { r: n >> 16, g: n >> 8 & 255, b: n & 255 };
379
379
  }
380
+ var HEX_3_RE = /^#([0-9a-fA-F]{3})$/;
381
+ var HEX_6_RE = /^#([0-9a-fA-F]{6})$/;
382
+ var HEX_8_RE = /^#([0-9a-fA-F]{8})$/;
383
+ var RGB_RE = /^rgba?\(\s*(-?\d{1,3})\s*,\s*(-?\d{1,3})\s*,\s*(-?\d{1,3})(?:\s*,\s*[\d.]+)?\s*\)$/i;
384
+ function parseColorToRgb(s) {
385
+ const trimmed = s.trim();
386
+ const m3 = HEX_3_RE.exec(trimmed);
387
+ if (m3) {
388
+ const [r, g, b] = m3[1];
389
+ return hexToRgb(`#${r}${r}${g}${g}${b}${b}`);
390
+ }
391
+ const m6 = HEX_6_RE.exec(trimmed);
392
+ if (m6) {
393
+ return hexToRgb(trimmed);
394
+ }
395
+ const m8 = HEX_8_RE.exec(trimmed);
396
+ if (m8) {
397
+ return hexToRgb(`#${trimmed.slice(1, 7)}`);
398
+ }
399
+ const mRgb = RGB_RE.exec(trimmed);
400
+ if (mRgb) {
401
+ return {
402
+ r: Math.max(0, Math.min(255, parseInt(mRgb[1], 10))),
403
+ g: Math.max(0, Math.min(255, parseInt(mRgb[2], 10))),
404
+ b: Math.max(0, Math.min(255, parseInt(mRgb[3], 10)))
405
+ };
406
+ }
407
+ return null;
408
+ }
409
+ var OKLCH_RE = /^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*[\d.]+)?\s*\)$/i;
410
+ function parseOklchToOklab(s) {
411
+ const m = OKLCH_RE.exec(s.trim());
412
+ if (!m) {
413
+ return null;
414
+ }
415
+ const L = parseFloat(m[1]);
416
+ const C = parseFloat(m[2]);
417
+ const H = parseFloat(m[3]);
418
+ if (Number.isNaN(L) || Number.isNaN(C) || Number.isNaN(H)) {
419
+ return null;
420
+ }
421
+ const clampedL = Math.max(0, Math.min(1, L));
422
+ const clampedC = Math.max(0, Math.min(0.4, C));
423
+ const H_rad = H * (Math.PI / 180);
424
+ return {
425
+ L: clampedL,
426
+ a: clampedC * Math.cos(H_rad),
427
+ b: clampedC * Math.sin(H_rad)
428
+ };
429
+ }
430
+ function parseColorToOklab(s) {
431
+ const oklab = parseOklchToOklab(s);
432
+ if (oklab !== null) {
433
+ return oklab;
434
+ }
435
+ const rgb = parseColorToRgb(s);
436
+ if (rgb === null) {
437
+ return null;
438
+ }
439
+ return rgbToOklab(rgb);
440
+ }
441
+ function colorToRgb(color) {
442
+ const rgb = parseColorToRgb(color);
443
+ if (rgb !== null) {
444
+ return rgb;
445
+ }
446
+ const lab = parseOklchToOklab(color);
447
+ if (lab !== null) {
448
+ return oklabToRgb(lab);
449
+ }
450
+ throw new Error(`[sarmal] unrecognized color "${color}"`);
451
+ }
380
452
  function srgbByteToLinear(c) {
381
453
  const n = c / 255;
382
454
  return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
@@ -414,29 +486,27 @@ var lerpOklab = (a, b, t) => {
414
486
  if (t >= 1) {
415
487
  return b;
416
488
  }
417
- const la = rgbToOklab(a), lb = rgbToOklab(b);
418
- return oklabToRgb({
419
- L: la.L + (lb.L - la.L) * t,
420
- a: la.a + (lb.a - la.a) * t,
421
- b: la.b + (lb.b - la.b) * t
422
- });
489
+ return {
490
+ L: a.L + (b.L - a.L) * t,
491
+ a: a.a + (b.a - a.a) * t,
492
+ b: a.b + (b.b - a.b) * t
493
+ };
423
494
  };
424
495
  function getPaletteColor(palette, position, timeOffset = 0) {
425
496
  if (palette.length === 0) {
426
- return { r: 255, g: 255, b: 255 };
497
+ return { L: 1, a: 0, b: 0 };
427
498
  }
428
499
  if (palette.length === 1) {
429
- return hexToRgb(palette[0]);
500
+ return palette[0];
430
501
  }
431
502
  const cyclePos = ((position + timeOffset) % 1 + 1) % 1;
432
503
  const scaled = cyclePos * palette.length;
433
504
  const idx = Math.floor(scaled);
434
505
  const t = scaled - idx;
435
- const c1 = hexToRgb(palette[idx % palette.length]);
436
- const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
506
+ const c1 = palette[idx % palette.length];
507
+ const c2 = palette[(idx + 1) % palette.length];
437
508
  return lerpOklab(c1, c2, t);
438
509
  }
439
- var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
440
510
  var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
441
511
  var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
442
512
  "trailColor",
@@ -469,9 +539,9 @@ function validateRenderOptions(partial) {
469
539
  }
470
540
  function assertTrailColor(value) {
471
541
  if (typeof value === "string") {
472
- if (!HEX_COLOR_RE.test(value)) {
542
+ if (parseColorToOklab(value) === null) {
473
543
  throw new TypeError(
474
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`
544
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()), got "${value}"`
475
545
  );
476
546
  }
477
547
  return;
@@ -484,25 +554,25 @@ function assertTrailColor(value) {
484
554
  }
485
555
  for (let i = 0; i < value.length; i++) {
486
556
  const entry = value[i];
487
- if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
557
+ if (typeof entry !== "string" || parseColorToOklab(entry) === null) {
488
558
  throw new TypeError(
489
- `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`
559
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()), got ${JSON.stringify(entry)}`
490
560
  );
491
561
  }
492
562
  }
493
563
  return;
494
564
  }
495
565
  throw new TypeError(
496
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`
566
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or an array of color strings, got ${JSON.stringify(value)}`
497
567
  );
498
568
  }
499
569
  function assertHeadColor(value) {
500
570
  if (value === null) {
501
571
  return;
502
572
  }
503
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
573
+ if (typeof value !== "string" || parseColorToOklab(value) === null) {
504
574
  throw new TypeError(
505
- `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`
575
+ `[sarmal] setRenderOptions: headColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or null, got ${JSON.stringify(value)}`
506
576
  );
507
577
  }
508
578
  }
@@ -510,9 +580,9 @@ function assertSkeletonColor(value) {
510
580
  if (value === "transparent") {
511
581
  return;
512
582
  }
513
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
583
+ if (typeof value !== "string" || parseColorToOklab(value) === null) {
514
584
  throw new TypeError(
515
- `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`
585
+ `[sarmal] setRenderOptions: skeletonColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba(), oklch()) or "transparent", got ${JSON.stringify(value)}`
516
586
  );
517
587
  }
518
588
  }
@@ -547,7 +617,7 @@ function resolveHeadColor(trailColor, trailStyle) {
547
617
  }
548
618
  const palette = resolveTrailPalette(trailColor);
549
619
  const last = palette[palette.length - 1];
550
- const { r, g, b } = hexToRgb(last);
620
+ const { r, g, b } = colorToRgb(last);
551
621
  return `rgb(${r},${g},${b})`;
552
622
  }
553
623
  function warnIfTrailColorMismatch(trailColor, trailStyle) {
@@ -567,9 +637,9 @@ function warnIfTrailColorMismatch(trailColor, trailStyle) {
567
637
  // src/renderer.ts
568
638
  var getHeadDotRadius = (w, h) => Math.max(1, 3 * Math.sqrt(Math.min(w, h) / 160));
569
639
  var WHITE_HEX = "#ffffff";
570
- function hexToRgbComponents(hex) {
571
- const n = parseInt(hex.slice(1), 16);
572
- return `${n >> 16},${n >> 8 & 255},${n & 255}`;
640
+ function colorToRgbComponents(color) {
641
+ const { r, g, b } = colorToRgb(color);
642
+ return `${r},${g},${b}`;
573
643
  }
574
644
  function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
575
645
  target.style.width = `${logicalWidth}px`;
@@ -589,8 +659,9 @@ function createRenderer(options) {
589
659
  let skeletonColor = options.skeletonColor ?? WHITE_HEX;
590
660
  let userHeadColor = options.headColor ?? null;
591
661
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
592
- let trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
662
+ let trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
593
663
  let trailPalette = resolveTrailPalette(trailColor);
664
+ let trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
594
665
  warnIfTrailColorMismatch(trailColor, trailStyle);
595
666
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
596
667
  function setupCanvas() {
@@ -634,7 +705,7 @@ function createRenderer(options) {
634
705
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
635
706
  const skeletonCtx = skeletonCanvas.getContext("2d");
636
707
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
637
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
708
+ skeletonCtx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
638
709
  skeletonCtx.lineWidth = 1.5;
639
710
  skeletonCtx.beginPath();
640
711
  const first = skeleton[0];
@@ -649,7 +720,7 @@ function createRenderer(options) {
649
720
  if (pts.length < 2) {
650
721
  return;
651
722
  }
652
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
723
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${opacity})`;
653
724
  ctx.lineWidth = 1.5;
654
725
  ctx.beginPath();
655
726
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -670,7 +741,7 @@ function createRenderer(options) {
670
741
  if (skeleton.length < 2) {
671
742
  return;
672
743
  }
673
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
744
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
674
745
  ctx.lineWidth = 1.5;
675
746
  ctx.beginPath();
676
747
  const first = skeleton[0];
@@ -702,8 +773,8 @@ function createRenderer(options) {
702
773
  ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
703
774
  } else {
704
775
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
705
- const color = getPaletteColor(trailPalette, progress, timeOffset);
706
- ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
776
+ const { r, g, b } = oklabToRgb(getPaletteColor(trailPaletteOklab, progress, timeOffset));
777
+ ctx.fillStyle = `rgba(${r},${g},${b},${opacity})`;
707
778
  }
708
779
  ctx.beginPath();
709
780
  ctx.moveTo(l0x, l0y);
@@ -747,7 +818,7 @@ function createRenderer(options) {
747
818
  morphReject = null;
748
819
  morphAlpha = 0;
749
820
  skeleton = engine.getSarmalSkeleton();
750
- if (!engine.isLiveSkeleton) {
821
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
751
822
  buildSkeletonCanvas();
752
823
  }
753
824
  }
@@ -773,7 +844,7 @@ function createRenderer(options) {
773
844
  }
774
845
  skeleton = engine.getSarmalSkeleton();
775
846
  calculateBoundaries();
776
- if (!engine.isLiveSkeleton) {
847
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
777
848
  buildSkeletonCanvas();
778
849
  }
779
850
  if (options.initialPhase !== void 0) {
@@ -835,8 +906,9 @@ function createRenderer(options) {
835
906
  validateRenderOptions(partial);
836
907
  if (partial.trailColor !== void 0) {
837
908
  trailColor = partial.trailColor;
838
- trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
909
+ trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
839
910
  trailPalette = resolveTrailPalette(trailColor);
911
+ trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
840
912
  }
841
913
  if (partial.skeletonColor !== void 0) {
842
914
  skeletonColor = partial.skeletonColor;
@@ -919,6 +991,10 @@ function sampleCurveSkeleton(curveDef) {
919
991
  function el(tag) {
920
992
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
921
993
  }
994
+ function colorToRgbAttr(color) {
995
+ const { r, g, b } = colorToRgb(color);
996
+ return `rgb(${r},${g},${b})`;
997
+ }
922
998
  function createSVGRenderer(options) {
923
999
  const { container, engine } = options;
924
1000
  const poolSize = engine.trailLength;
@@ -933,8 +1009,9 @@ function createSVGRenderer(options) {
933
1009
  let userHeadColor = options.headColor ?? null;
934
1010
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
935
1011
  let headRadius;
936
- let trailSolid = resolveTrailMainColor(trailColor);
1012
+ let trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
937
1013
  let trailPalette = resolveTrailPalette(trailColor);
1014
+ let trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
938
1015
  const ariaLabel = options.ariaLabel ?? "Loading";
939
1016
  warnIfTrailColorMismatch(trailColor, trailStyle);
940
1017
  const viewSize = 100;
@@ -957,7 +1034,10 @@ function createSVGRenderer(options) {
957
1034
  const skeletonPath = el("path");
958
1035
  skeletonPath.setAttribute("data-sarmal-role", "skeleton");
959
1036
  skeletonPath.setAttribute("fill", "none");
960
- skeletonPath.setAttribute("stroke", skeletonColor);
1037
+ skeletonPath.setAttribute(
1038
+ "stroke",
1039
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1040
+ );
961
1041
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
962
1042
  skeletonPath.setAttribute("stroke-width", svgSkeletonStrokeWidth);
963
1043
  if (skeletonColor === "transparent") {
@@ -966,13 +1046,19 @@ function createSVGRenderer(options) {
966
1046
  group.appendChild(skeletonPath);
967
1047
  const skeletonPathA = el("path");
968
1048
  skeletonPathA.setAttribute("fill", "none");
969
- skeletonPathA.setAttribute("stroke", skeletonColor);
1049
+ skeletonPathA.setAttribute(
1050
+ "stroke",
1051
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1052
+ );
970
1053
  skeletonPathA.setAttribute("stroke-width", svgSkeletonStrokeWidth);
971
1054
  skeletonPathA.setAttribute("visibility", "hidden");
972
1055
  group.appendChild(skeletonPathA);
973
1056
  const skeletonPathB = el("path");
974
1057
  skeletonPathB.setAttribute("fill", "none");
975
- skeletonPathB.setAttribute("stroke", skeletonColor);
1058
+ skeletonPathB.setAttribute(
1059
+ "stroke",
1060
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1061
+ );
976
1062
  skeletonPathB.setAttribute("stroke-width", svgSkeletonStrokeWidth);
977
1063
  skeletonPathB.setAttribute("visibility", "hidden");
978
1064
  group.appendChild(skeletonPathB);
@@ -987,7 +1073,7 @@ function createSVGRenderer(options) {
987
1073
  }
988
1074
  const headCircle = el("circle");
989
1075
  headCircle.setAttribute("data-sarmal-role", "head");
990
- headCircle.setAttribute("fill", headColor);
1076
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
991
1077
  headCircle.setAttribute("r", String(headRadius));
992
1078
  group.appendChild(headCircle);
993
1079
  container.appendChild(group);
@@ -1040,7 +1126,7 @@ function createSVGRenderer(options) {
1040
1126
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
1041
1127
  if (trailStyle !== "default") {
1042
1128
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
1043
- const { r, g, b } = getPaletteColor(trailPalette, progress, timeOffset);
1129
+ const { r, g, b } = oklabToRgb(getPaletteColor(trailPaletteOklab, progress, timeOffset));
1044
1130
  trailPaths[i].setAttribute("fill", `rgb(${r},${g},${b})`);
1045
1131
  }
1046
1132
  }
@@ -1190,8 +1276,9 @@ function createSVGRenderer(options) {
1190
1276
  const prevTrailStyle = trailStyle;
1191
1277
  if (partial.trailColor !== void 0) {
1192
1278
  trailColor = partial.trailColor;
1193
- trailSolid = resolveTrailMainColor(trailColor);
1279
+ trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
1194
1280
  trailPalette = resolveTrailPalette(trailColor);
1281
+ trailPaletteOklab = trailPalette.map((c) => parseColorToOklab(c));
1195
1282
  if (trailStyle === "default") {
1196
1283
  for (const p of trailPaths) {
1197
1284
  p.setAttribute("fill", trailSolid);
@@ -1203,10 +1290,10 @@ function createSVGRenderer(options) {
1203
1290
  if (skeletonColor === "transparent") {
1204
1291
  skeletonPath.setAttribute("visibility", "hidden");
1205
1292
  } else {
1206
- skeletonPath.setAttribute("stroke", skeletonColor);
1293
+ skeletonPath.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1207
1294
  skeletonPath.removeAttribute("visibility");
1208
- skeletonPathA.setAttribute("stroke", skeletonColor);
1209
- skeletonPathB.setAttribute("stroke", skeletonColor);
1295
+ skeletonPathA.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1296
+ skeletonPathB.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1210
1297
  }
1211
1298
  }
1212
1299
  if (partial.trailStyle !== void 0) {
@@ -1229,7 +1316,7 @@ function createSVGRenderer(options) {
1229
1316
  } else {
1230
1317
  headColor = userHeadColor;
1231
1318
  }
1232
- headCircle.setAttribute("fill", headColor);
1319
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
1233
1320
  if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1234
1321
  warnIfTrailColorMismatch(trailColor, trailStyle);
1235
1322
  }