@sarmal/core 0.14.0 → 0.15.1

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
@@ -369,7 +369,7 @@ function enginePassthroughs(engine) {
369
369
  setSpeedOver: engine.setSpeedOver
370
370
  };
371
371
  }
372
- var GRADIENT = {
372
+ var palettes = {
373
373
  bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
374
374
  sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
375
375
  ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
@@ -377,14 +377,6 @@ var GRADIENT = {
377
377
  fire: ["#7f1d1d", "#fbbf24"],
378
378
  forest: ["#14532d", "#86efac"]
379
379
  };
380
- var PRESETS = {
381
- bard: GRADIENT.bard,
382
- sunset: GRADIENT.sunset,
383
- ocean: GRADIENT.ocean,
384
- ice: GRADIENT.ice,
385
- fire: GRADIENT.fire,
386
- forest: GRADIENT.forest
387
- };
388
380
  function hexToRgb(hex) {
389
381
  const n = parseInt(hex.slice(1), 16);
390
382
  return { r: n >> 16, g: n >> 8 & 255, b: n & 255 };
@@ -409,18 +401,121 @@ function getPaletteColor(palette, position, timeOffset = 0) {
409
401
  const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
410
402
  return lerpRgb(c1, c2, t);
411
403
  }
412
- function resolvePalette(palette, trailStyle) {
413
- if (Array.isArray(palette)) {
414
- return palette;
404
+ var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
405
+ var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
406
+ var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
407
+ "trailColor",
408
+ "headColor",
409
+ "skeletonColor",
410
+ "trailStyle"
411
+ ]);
412
+ function validateRenderOptions(partial) {
413
+ for (const key of Object.keys(partial)) {
414
+ if (!RENDER_OPTION_KEYS.has(key)) {
415
+ throw new TypeError(`[sarmal] setRenderOptions: unknown key "${key}"`);
416
+ }
417
+ }
418
+ if (partial.trailColor !== void 0) {
419
+ assertTrailColor(partial.trailColor);
420
+ }
421
+ if (partial.headColor !== void 0) {
422
+ assertHeadColor(partial.headColor);
423
+ }
424
+ if (partial.skeletonColor !== void 0) {
425
+ assertSkeletonColor(partial.skeletonColor);
426
+ }
427
+ if (partial.trailStyle !== void 0) {
428
+ assertTrailStyle(partial.trailStyle);
429
+ }
430
+ }
431
+ function assertTrailColor(value) {
432
+ if (typeof value === "string") {
433
+ if (!HEX_COLOR_RE.test(value)) {
434
+ throw new TypeError(
435
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`
436
+ );
437
+ }
438
+ return;
415
439
  }
416
- if (palette && palette in PRESETS) {
417
- return PRESETS[palette];
440
+ if (Array.isArray(value)) {
441
+ if (value.length < 2) {
442
+ throw new RangeError(
443
+ `[sarmal] setRenderOptions: trailColor array must have at least 2 entries, got ${value.length}`
444
+ );
445
+ }
446
+ for (let i = 0; i < value.length; i++) {
447
+ const entry = value[i];
448
+ if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
449
+ throw new TypeError(
450
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`
451
+ );
452
+ }
453
+ }
454
+ return;
418
455
  }
419
- return trailStyle === "gradient-animated" ? GRADIENT.bard : GRADIENT.ice;
456
+ throw new TypeError(
457
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`
458
+ );
420
459
  }
460
+ function assertHeadColor(value) {
461
+ if (value === null) {
462
+ return;
463
+ }
464
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
465
+ throw new TypeError(
466
+ `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`
467
+ );
468
+ }
469
+ }
470
+ function assertSkeletonColor(value) {
471
+ if (value === "transparent") {
472
+ return;
473
+ }
474
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
475
+ throw new TypeError(
476
+ `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`
477
+ );
478
+ }
479
+ }
480
+ function assertTrailStyle(value) {
481
+ if (!TRAIL_STYLES.includes(value)) {
482
+ throw new RangeError(
483
+ `[sarmal] setRenderOptions: trailStyle must be one of "default", "gradient-static", "gradient-animated", got ${JSON.stringify(value)}`
484
+ );
485
+ }
486
+ }
487
+ function resolveTrailMainColor(trailColor) {
488
+ return typeof trailColor === "string" ? trailColor : trailColor[0];
489
+ }
490
+ function resolveTrailPalette(trailColor) {
491
+ return typeof trailColor === "string" ? [trailColor] : trailColor;
492
+ }
493
+ function resolveHeadColor(trailColor, trailStyle) {
494
+ if (trailStyle === "default") {
495
+ return resolveTrailMainColor(trailColor);
496
+ }
497
+ const palette = resolveTrailPalette(trailColor);
498
+ const last = palette[palette.length - 1];
499
+ const { r, g, b } = hexToRgb(last);
500
+ return `rgb(${r},${g},${b})`;
501
+ }
502
+ function warnIfTrailColorMismatch(trailColor, trailStyle) {
503
+ if (trailStyle === "default" && Array.isArray(trailColor)) {
504
+ console.warn(
505
+ '[sarmal] trailColor is an array but trailStyle is "default"; only the first color will be used. Pass a gradient trailStyle to use the whole palette.'
506
+ );
507
+ return;
508
+ }
509
+ if (trailStyle !== "default" && typeof trailColor === "string") {
510
+ console.warn(
511
+ `[sarmal] trailColor is a single color but trailStyle is "${trailStyle}"; the trail will render as a solid color. Pass an array of hex colors to use a real gradient.`
512
+ );
513
+ }
514
+ }
515
+ var getHeadDotRadius = (w, h) => Math.max(1, 3 * Math.sqrt(Math.min(w, h) / 160));
421
516
 
422
517
  // src/renderer.ts
423
- var DEFAULT_SKELETON_COLOR = "#ffffff";
518
+ var WHITE_HEX = "#ffffff";
424
519
  function hexToRgbComponents(hex) {
425
520
  const n = parseInt(hex.slice(1), 16);
426
521
  return `${n >> 16},${n >> 8 & 255},${n & 255}`;
@@ -438,27 +533,18 @@ function createRenderer(options) {
438
533
  }
439
534
  const ctx = canvas.getContext("2d");
440
535
  const engine = options.engine;
441
- const trailStyle = options.trailStyle ?? "default";
442
- const trailColor = options.trailColor ?? "#ffffff";
443
- const palette = resolvePalette(options.palette, trailStyle);
444
- function defaultHeadColor() {
445
- if (trailStyle !== "default") {
446
- const { r, g, b } = getPaletteColor(palette, 1);
447
- return `rgb(${r},${g},${b})`;
448
- }
449
- return trailColor;
450
- }
451
- const opts = {
452
- skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,
453
- trailColor,
454
- headColor: options.headColor ?? defaultHeadColor()
455
- };
456
- const trailRgb = hexToRgbComponents(opts.trailColor);
536
+ let trailStyle = options.trailStyle ?? "default";
537
+ let trailColor = options.trailColor ?? WHITE_HEX;
538
+ let skeletonColor = options.skeletonColor ?? WHITE_HEX;
539
+ let userHeadColor = options.headColor ?? null;
540
+ let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
541
+ let trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
542
+ let trailPalette = resolveTrailPalette(trailColor);
543
+ warnIfTrailColorMismatch(trailColor, trailStyle);
457
544
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
458
545
  function setupCanvas() {
459
- const rect = canvas.getBoundingClientRect();
460
- const lw = rect.width || 200;
461
- const lh = rect.height || 200;
546
+ const lw = canvas.offsetWidth || 200;
547
+ const lh = canvas.offsetHeight || 200;
462
548
  applyDprSizing(canvas, lw, lh, dpr);
463
549
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
464
550
  }
@@ -488,11 +574,13 @@ function createRenderer(options) {
488
574
  }
489
575
  }
490
576
  function buildSkeletonCanvas() {
491
- if (skeleton.length < 2) return;
577
+ if (skeleton.length < 2) {
578
+ return;
579
+ }
492
580
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
493
581
  const skeletonCtx = skeletonCanvas.getContext("2d");
494
582
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
495
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
583
+ skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
496
584
  skeletonCtx.lineWidth = 1.5;
497
585
  skeletonCtx.beginPath();
498
586
  const first = skeleton[0];
@@ -504,8 +592,10 @@ function createRenderer(options) {
504
592
  skeletonCtx.stroke();
505
593
  }
506
594
  function drawSkeletonPath(pts, opacity) {
507
- if (pts.length < 2) return;
508
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${opacity})`;
595
+ if (pts.length < 2) {
596
+ return;
597
+ }
598
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
509
599
  ctx.lineWidth = 1.5;
510
600
  ctx.beginPath();
511
601
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -515,7 +605,7 @@ function createRenderer(options) {
515
605
  ctx.stroke();
516
606
  }
517
607
  function drawSkeleton() {
518
- if (opts.skeletonColor === "transparent") {
608
+ if (skeletonColor === "transparent") {
519
609
  return;
520
610
  }
521
611
  if (engine.morphAlpha !== null) {
@@ -526,7 +616,7 @@ function createRenderer(options) {
526
616
  if (skeleton.length < 2) {
527
617
  return;
528
618
  }
529
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
619
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
530
620
  ctx.lineWidth = 1.5;
531
621
  ctx.beginPath();
532
622
  const first = skeleton[0];
@@ -555,10 +645,10 @@ function createRenderer(options) {
555
645
  toY
556
646
  );
557
647
  if (trailStyle === "default") {
558
- ctx.fillStyle = `rgba(${trailRgb},${opacity})`;
648
+ ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
559
649
  } else {
560
650
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
561
- const color = getPaletteColor(palette, progress, timeOffset);
651
+ const color = getPaletteColor(trailPalette, progress, timeOffset);
562
652
  ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
563
653
  }
564
654
  ctx.beginPath();
@@ -576,8 +666,8 @@ function createRenderer(options) {
576
666
  }
577
667
  const x = head.x * scale + offsetX;
578
668
  const y = head.y * scale + offsetY;
579
- const r = options.headRadius ?? Math.max(2, 3 * Math.sqrt(Math.min(logicalWidth, logicalHeight) / 160));
580
- ctx.fillStyle = opts.headColor;
669
+ const r = options.headRadius ?? getHeadDotRadius(logicalWidth, logicalHeight);
670
+ ctx.fillStyle = headColor;
581
671
  ctx.beginPath();
582
672
  ctx.arc(x, y, r, 0, Math.PI * 2);
583
673
  ctx.fill();
@@ -677,6 +767,34 @@ function createRenderer(options) {
677
767
  return new Promise((resolve) => {
678
768
  morphResolve = resolve;
679
769
  });
770
+ },
771
+ setRenderOptions(partial) {
772
+ validateRenderOptions(partial);
773
+ if (partial.trailColor !== void 0) {
774
+ trailColor = partial.trailColor;
775
+ trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
776
+ trailPalette = resolveTrailPalette(trailColor);
777
+ }
778
+ if (partial.skeletonColor !== void 0) {
779
+ skeletonColor = partial.skeletonColor;
780
+ if (skeletonColor !== "transparent" && !engine.isLiveSkeleton) {
781
+ buildSkeletonCanvas();
782
+ }
783
+ }
784
+ if (partial.trailStyle !== void 0) {
785
+ trailStyle = partial.trailStyle;
786
+ }
787
+ if (partial.headColor !== void 0) {
788
+ userHeadColor = partial.headColor;
789
+ }
790
+ if (userHeadColor === null) {
791
+ headColor = resolveHeadColor(trailColor, trailStyle);
792
+ } else {
793
+ headColor = userHeadColor;
794
+ }
795
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
796
+ warnIfTrailColorMismatch(trailColor, trailStyle);
797
+ }
680
798
  }
681
799
  };
682
800
  if (shouldAutoStart) {
@@ -713,47 +831,47 @@ function el(tag) {
713
831
  }
714
832
  function createSVGRenderer(options) {
715
833
  const { container, engine } = options;
716
- const trailColor = options.trailColor ?? "#ffffff";
717
- const trailStyle = options.trailStyle ?? "default";
718
- const palette = resolvePalette(options.palette, trailStyle);
719
- const opts = {
720
- skeletonColor: options.skeletonColor ?? "#ffffff",
721
- trailColor,
722
- headColor: options.headColor ?? (trailStyle !== "default" ? (() => {
723
- const { r, g, b } = getPaletteColor(palette, 1);
724
- return `rgb(${r},${g},${b})`;
725
- })() : trailColor),
726
- ariaLabel: options.ariaLabel ?? "Loading"
727
- };
728
- const rect = container.getBoundingClientRect();
729
- const width = rect.width || 200;
730
- const height = rect.height || 200;
731
- const headRadius = options.headRadius ?? Math.max(2, 3 * Math.sqrt(Math.min(width, height) / 160));
834
+ let trailStyle = options.trailStyle ?? "default";
835
+ let trailColor = options.trailColor ?? "#ffffff";
836
+ let skeletonColor = options.skeletonColor ?? "#ffffff";
837
+ let userHeadColor = options.headColor ?? null;
838
+ let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
839
+ let trailSolid = resolveTrailMainColor(trailColor);
840
+ let trailPalette = resolveTrailPalette(trailColor);
841
+ const ariaLabel = options.ariaLabel ?? "Loading";
842
+ warnIfTrailColorMismatch(trailColor, trailStyle);
843
+ const htmlContainer = container;
844
+ const width = htmlContainer.offsetWidth || 200;
845
+ const height = htmlContainer.offsetHeight || 200;
846
+ const headRadius = options.headRadius ?? getHeadDotRadius(width, height);
732
847
  const svg = el("svg");
733
848
  svg.setAttribute("width", String(width));
734
849
  svg.setAttribute("height", String(height));
735
850
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
736
851
  svg.setAttribute("role", "img");
737
- svg.setAttribute("aria-label", opts.ariaLabel);
852
+ svg.setAttribute("aria-label", ariaLabel);
738
853
  const titleEl = el("title");
739
- titleEl.textContent = opts.ariaLabel;
854
+ titleEl.textContent = ariaLabel;
740
855
  svg.appendChild(titleEl);
741
856
  const skeletonPath = el("path");
742
857
  skeletonPath.setAttribute("data-sarmal-role", "skeleton");
743
858
  skeletonPath.setAttribute("fill", "none");
744
- skeletonPath.setAttribute("stroke", opts.skeletonColor);
859
+ skeletonPath.setAttribute("stroke", skeletonColor);
745
860
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
746
861
  skeletonPath.setAttribute("stroke-width", "1.5");
862
+ if (skeletonColor === "transparent") {
863
+ skeletonPath.setAttribute("visibility", "hidden");
864
+ }
747
865
  svg.appendChild(skeletonPath);
748
866
  const skeletonPathA = el("path");
749
867
  skeletonPathA.setAttribute("fill", "none");
750
- skeletonPathA.setAttribute("stroke", opts.skeletonColor);
868
+ skeletonPathA.setAttribute("stroke", skeletonColor);
751
869
  skeletonPathA.setAttribute("stroke-width", "1.5");
752
870
  skeletonPathA.setAttribute("visibility", "hidden");
753
871
  svg.appendChild(skeletonPathA);
754
872
  const skeletonPathB = el("path");
755
873
  skeletonPathB.setAttribute("fill", "none");
756
- skeletonPathB.setAttribute("stroke", opts.skeletonColor);
874
+ skeletonPathB.setAttribute("stroke", skeletonColor);
757
875
  skeletonPathB.setAttribute("stroke-width", "1.5");
758
876
  skeletonPathB.setAttribute("visibility", "hidden");
759
877
  svg.appendChild(skeletonPathB);
@@ -762,13 +880,13 @@ function createSVGRenderer(options) {
762
880
  const trailPaths = [];
763
881
  for (let i = 0; i < MAX_TRAIL_SEGMENTS; i++) {
764
882
  const path = el("path");
765
- path.setAttribute("fill", opts.trailColor);
883
+ path.setAttribute("fill", trailSolid);
766
884
  svg.appendChild(path);
767
885
  trailPaths.push(path);
768
886
  }
769
887
  const headCircle = el("circle");
770
888
  headCircle.setAttribute("data-sarmal-role", "head");
771
- headCircle.setAttribute("fill", opts.headColor);
889
+ headCircle.setAttribute("fill", headColor);
772
890
  headCircle.setAttribute("r", String(headRadius));
773
891
  svg.appendChild(headCircle);
774
892
  container.appendChild(svg);
@@ -818,7 +936,7 @@ function createSVGRenderer(options) {
818
936
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
819
937
  if (trailStyle !== "default") {
820
938
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
821
- const { r, g, b } = getPaletteColor(palette, progress, timeOffset);
939
+ const { r, g, b } = getPaletteColor(trailPalette, progress, timeOffset);
822
940
  trailPaths[i].setAttribute("fill", `rgb(${r},${g},${b})`);
823
941
  }
824
942
  }
@@ -951,6 +1069,51 @@ function createSVGRenderer(options) {
951
1069
  return new Promise((resolve) => {
952
1070
  morphResolve = resolve;
953
1071
  });
1072
+ },
1073
+ setRenderOptions(partial) {
1074
+ validateRenderOptions(partial);
1075
+ const prevTrailStyle = trailStyle;
1076
+ if (partial.trailColor !== void 0) {
1077
+ trailColor = partial.trailColor;
1078
+ trailSolid = resolveTrailMainColor(trailColor);
1079
+ trailPalette = resolveTrailPalette(trailColor);
1080
+ if (trailStyle === "default") {
1081
+ for (const p of trailPaths) {
1082
+ p.setAttribute("fill", trailSolid);
1083
+ }
1084
+ }
1085
+ }
1086
+ if (partial.skeletonColor !== void 0) {
1087
+ skeletonColor = partial.skeletonColor;
1088
+ if (skeletonColor === "transparent") {
1089
+ skeletonPath.setAttribute("visibility", "hidden");
1090
+ } else {
1091
+ skeletonPath.setAttribute("stroke", skeletonColor);
1092
+ skeletonPath.removeAttribute("visibility");
1093
+ skeletonPathA.setAttribute("stroke", skeletonColor);
1094
+ skeletonPathB.setAttribute("stroke", skeletonColor);
1095
+ }
1096
+ }
1097
+ if (partial.trailStyle !== void 0) {
1098
+ trailStyle = partial.trailStyle;
1099
+ if (prevTrailStyle !== "default" && trailStyle === "default") {
1100
+ for (const p of trailPaths) {
1101
+ p.setAttribute("fill", trailSolid);
1102
+ }
1103
+ }
1104
+ }
1105
+ if (partial.headColor !== void 0) {
1106
+ userHeadColor = partial.headColor;
1107
+ }
1108
+ if (userHeadColor === null) {
1109
+ headColor = resolveHeadColor(trailColor, trailStyle);
1110
+ } else {
1111
+ headColor = userHeadColor;
1112
+ }
1113
+ headCircle.setAttribute("fill", headColor);
1114
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1115
+ warnIfTrailColorMismatch(trailColor, trailStyle);
1116
+ }
954
1117
  }
955
1118
  };
956
1119
  if (shouldAutoStart) {
@@ -1172,6 +1335,7 @@ exports.epitrochoid7 = epitrochoid7;
1172
1335
  exports.lame = lame;
1173
1336
  exports.lissajous32 = lissajous32;
1174
1337
  exports.lissajous43 = lissajous43;
1338
+ exports.palettes = palettes;
1175
1339
  exports.rose3 = rose3;
1176
1340
  exports.rose5 = rose5;
1177
1341
  //# sourceMappingURL=index.cjs.map