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