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