@sarmal/core 0.29.0 → 0.30.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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  The animations can be used anywhere you want. Use it for loading spinners, progress indicators, or to indicate that your very special AI model is _thinking_, up to you.
16
16
 
17
- In web applications and perhaps also in the terminal maybe if possible!
17
+ In web applications or directly in your terminal (`npx @sarmal/core`).
18
18
 
19
19
  - **Canvas & SVG renderers**: choose one or the other, but why not both?
20
20
  - **standard curves**: default cliche curves any LLM can generate in seconds, from classic spirals to custom parametric paths
@@ -377,6 +377,35 @@ 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
+ }
380
409
  function srgbByteToLinear(c) {
381
410
  const n = c / 255;
382
411
  return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
@@ -436,7 +465,6 @@ function getPaletteColor(palette, position, timeOffset = 0) {
436
465
  const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
437
466
  return lerpOklab(c1, c2, t);
438
467
  }
439
- var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
440
468
  var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
441
469
  var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
442
470
  "trailColor",
@@ -469,9 +497,9 @@ function validateRenderOptions(partial) {
469
497
  }
470
498
  function assertTrailColor(value) {
471
499
  if (typeof value === "string") {
472
- if (!HEX_COLOR_RE.test(value)) {
500
+ if (parseColorToRgb(value) === null) {
473
501
  throw new TypeError(
474
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`
502
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba()), got "${value}"`
475
503
  );
476
504
  }
477
505
  return;
@@ -484,25 +512,25 @@ function assertTrailColor(value) {
484
512
  }
485
513
  for (let i = 0; i < value.length; i++) {
486
514
  const entry = value[i];
487
- if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
515
+ if (typeof entry !== "string" || parseColorToRgb(entry) === null) {
488
516
  throw new TypeError(
489
- `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`
517
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a valid color string (#rrggbb, #rgb, rgb(), rgba()), got ${JSON.stringify(entry)}`
490
518
  );
491
519
  }
492
520
  }
493
521
  return;
494
522
  }
495
523
  throw new TypeError(
496
- `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`
524
+ `[sarmal] setRenderOptions: trailColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba()) or an array of color strings, got ${JSON.stringify(value)}`
497
525
  );
498
526
  }
499
527
  function assertHeadColor(value) {
500
528
  if (value === null) {
501
529
  return;
502
530
  }
503
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
531
+ if (typeof value !== "string" || parseColorToRgb(value) === null) {
504
532
  throw new TypeError(
505
- `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`
533
+ `[sarmal] setRenderOptions: headColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba()) or null, got ${JSON.stringify(value)}`
506
534
  );
507
535
  }
508
536
  }
@@ -510,9 +538,9 @@ function assertSkeletonColor(value) {
510
538
  if (value === "transparent") {
511
539
  return;
512
540
  }
513
- if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
541
+ if (typeof value !== "string" || parseColorToRgb(value) === null) {
514
542
  throw new TypeError(
515
- `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`
543
+ `[sarmal] setRenderOptions: skeletonColor must be a valid color string (#rrggbb, #rgb, rgb(), rgba()) or "transparent", got ${JSON.stringify(value)}`
516
544
  );
517
545
  }
518
546
  }
@@ -547,7 +575,7 @@ function resolveHeadColor(trailColor, trailStyle) {
547
575
  }
548
576
  const palette = resolveTrailPalette(trailColor);
549
577
  const last = palette[palette.length - 1];
550
- const { r, g, b } = hexToRgb(last);
578
+ const { r, g, b } = parseColorToRgb(last);
551
579
  return `rgb(${r},${g},${b})`;
552
580
  }
553
581
  function warnIfTrailColorMismatch(trailColor, trailStyle) {
@@ -567,9 +595,9 @@ function warnIfTrailColorMismatch(trailColor, trailStyle) {
567
595
  // src/renderer.ts
568
596
  var getHeadDotRadius = (w, h) => Math.max(1, 3 * Math.sqrt(Math.min(w, h) / 160));
569
597
  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}`;
598
+ function colorToRgbComponents(color) {
599
+ const c = parseColorToRgb(color);
600
+ return `${c.r},${c.g},${c.b}`;
573
601
  }
574
602
  function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
575
603
  target.style.width = `${logicalWidth}px`;
@@ -589,7 +617,7 @@ function createRenderer(options) {
589
617
  let skeletonColor = options.skeletonColor ?? WHITE_HEX;
590
618
  let userHeadColor = options.headColor ?? null;
591
619
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
592
- let trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
620
+ let trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
593
621
  let trailPalette = resolveTrailPalette(trailColor);
594
622
  warnIfTrailColorMismatch(trailColor, trailStyle);
595
623
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
@@ -634,7 +662,7 @@ function createRenderer(options) {
634
662
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
635
663
  const skeletonCtx = skeletonCanvas.getContext("2d");
636
664
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
637
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
665
+ skeletonCtx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
638
666
  skeletonCtx.lineWidth = 1.5;
639
667
  skeletonCtx.beginPath();
640
668
  const first = skeleton[0];
@@ -649,7 +677,7 @@ function createRenderer(options) {
649
677
  if (pts.length < 2) {
650
678
  return;
651
679
  }
652
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
680
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${opacity})`;
653
681
  ctx.lineWidth = 1.5;
654
682
  ctx.beginPath();
655
683
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -670,7 +698,7 @@ function createRenderer(options) {
670
698
  if (skeleton.length < 2) {
671
699
  return;
672
700
  }
673
- ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
701
+ ctx.strokeStyle = `rgba(${colorToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
674
702
  ctx.lineWidth = 1.5;
675
703
  ctx.beginPath();
676
704
  const first = skeleton[0];
@@ -747,7 +775,7 @@ function createRenderer(options) {
747
775
  morphReject = null;
748
776
  morphAlpha = 0;
749
777
  skeleton = engine.getSarmalSkeleton();
750
- if (!engine.isLiveSkeleton) {
778
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
751
779
  buildSkeletonCanvas();
752
780
  }
753
781
  }
@@ -773,7 +801,7 @@ function createRenderer(options) {
773
801
  }
774
802
  skeleton = engine.getSarmalSkeleton();
775
803
  calculateBoundaries();
776
- if (!engine.isLiveSkeleton) {
804
+ if (!engine.isLiveSkeleton && skeletonColor !== "transparent") {
777
805
  buildSkeletonCanvas();
778
806
  }
779
807
  if (options.initialPhase !== void 0) {
@@ -835,7 +863,7 @@ function createRenderer(options) {
835
863
  validateRenderOptions(partial);
836
864
  if (partial.trailColor !== void 0) {
837
865
  trailColor = partial.trailColor;
838
- trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
866
+ trailSolidRgb = colorToRgbComponents(resolveTrailMainColor(trailColor));
839
867
  trailPalette = resolveTrailPalette(trailColor);
840
868
  }
841
869
  if (partial.skeletonColor !== void 0) {
@@ -919,6 +947,10 @@ function sampleCurveSkeleton(curveDef) {
919
947
  function el(tag) {
920
948
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
921
949
  }
950
+ function colorToRgbAttr(color) {
951
+ const c = parseColorToRgb(color);
952
+ return `rgb(${c.r},${c.g},${c.b})`;
953
+ }
922
954
  function createSVGRenderer(options) {
923
955
  const { container, engine } = options;
924
956
  const poolSize = engine.trailLength;
@@ -933,7 +965,7 @@ function createSVGRenderer(options) {
933
965
  let userHeadColor = options.headColor ?? null;
934
966
  let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
935
967
  let headRadius;
936
- let trailSolid = resolveTrailMainColor(trailColor);
968
+ let trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
937
969
  let trailPalette = resolveTrailPalette(trailColor);
938
970
  const ariaLabel = options.ariaLabel ?? "Loading";
939
971
  warnIfTrailColorMismatch(trailColor, trailStyle);
@@ -957,7 +989,10 @@ function createSVGRenderer(options) {
957
989
  const skeletonPath = el("path");
958
990
  skeletonPath.setAttribute("data-sarmal-role", "skeleton");
959
991
  skeletonPath.setAttribute("fill", "none");
960
- skeletonPath.setAttribute("stroke", skeletonColor);
992
+ skeletonPath.setAttribute(
993
+ "stroke",
994
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
995
+ );
961
996
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
962
997
  skeletonPath.setAttribute("stroke-width", svgSkeletonStrokeWidth);
963
998
  if (skeletonColor === "transparent") {
@@ -966,13 +1001,19 @@ function createSVGRenderer(options) {
966
1001
  group.appendChild(skeletonPath);
967
1002
  const skeletonPathA = el("path");
968
1003
  skeletonPathA.setAttribute("fill", "none");
969
- skeletonPathA.setAttribute("stroke", skeletonColor);
1004
+ skeletonPathA.setAttribute(
1005
+ "stroke",
1006
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1007
+ );
970
1008
  skeletonPathA.setAttribute("stroke-width", svgSkeletonStrokeWidth);
971
1009
  skeletonPathA.setAttribute("visibility", "hidden");
972
1010
  group.appendChild(skeletonPathA);
973
1011
  const skeletonPathB = el("path");
974
1012
  skeletonPathB.setAttribute("fill", "none");
975
- skeletonPathB.setAttribute("stroke", skeletonColor);
1013
+ skeletonPathB.setAttribute(
1014
+ "stroke",
1015
+ skeletonColor === "transparent" ? "transparent" : colorToRgbAttr(skeletonColor)
1016
+ );
976
1017
  skeletonPathB.setAttribute("stroke-width", svgSkeletonStrokeWidth);
977
1018
  skeletonPathB.setAttribute("visibility", "hidden");
978
1019
  group.appendChild(skeletonPathB);
@@ -987,7 +1028,7 @@ function createSVGRenderer(options) {
987
1028
  }
988
1029
  const headCircle = el("circle");
989
1030
  headCircle.setAttribute("data-sarmal-role", "head");
990
- headCircle.setAttribute("fill", headColor);
1031
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
991
1032
  headCircle.setAttribute("r", String(headRadius));
992
1033
  group.appendChild(headCircle);
993
1034
  container.appendChild(group);
@@ -1190,7 +1231,7 @@ function createSVGRenderer(options) {
1190
1231
  const prevTrailStyle = trailStyle;
1191
1232
  if (partial.trailColor !== void 0) {
1192
1233
  trailColor = partial.trailColor;
1193
- trailSolid = resolveTrailMainColor(trailColor);
1234
+ trailSolid = colorToRgbAttr(resolveTrailMainColor(trailColor));
1194
1235
  trailPalette = resolveTrailPalette(trailColor);
1195
1236
  if (trailStyle === "default") {
1196
1237
  for (const p of trailPaths) {
@@ -1203,10 +1244,10 @@ function createSVGRenderer(options) {
1203
1244
  if (skeletonColor === "transparent") {
1204
1245
  skeletonPath.setAttribute("visibility", "hidden");
1205
1246
  } else {
1206
- skeletonPath.setAttribute("stroke", skeletonColor);
1247
+ skeletonPath.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1207
1248
  skeletonPath.removeAttribute("visibility");
1208
- skeletonPathA.setAttribute("stroke", skeletonColor);
1209
- skeletonPathB.setAttribute("stroke", skeletonColor);
1249
+ skeletonPathA.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1250
+ skeletonPathB.setAttribute("stroke", colorToRgbAttr(skeletonColor));
1210
1251
  }
1211
1252
  }
1212
1253
  if (partial.trailStyle !== void 0) {
@@ -1229,7 +1270,7 @@ function createSVGRenderer(options) {
1229
1270
  } else {
1230
1271
  headColor = userHeadColor;
1231
1272
  }
1232
- headCircle.setAttribute("fill", headColor);
1273
+ headCircle.setAttribute("fill", colorToRgbAttr(headColor));
1233
1274
  if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1234
1275
  warnIfTrailColorMismatch(trailColor, trailStyle);
1235
1276
  }