@sarmal/core 0.13.0 → 0.15.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.cjs CHANGED
@@ -378,10 +378,7 @@ function enginePassthroughs(engine) {
378
378
  setSpeedOver: engine.setSpeedOver,
379
379
  };
380
380
  }
381
-
382
- // src/renderer.ts
383
- var DEFAULT_SKELETON_COLOR = "#ffffff";
384
- var GRADIENT = {
381
+ var palettes = {
385
382
  bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
386
383
  sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
387
384
  ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
@@ -389,14 +386,6 @@ var GRADIENT = {
389
386
  fire: ["#7f1d1d", "#fbbf24"],
390
387
  forest: ["#14532d", "#86efac"],
391
388
  };
392
- var PRESETS = {
393
- bard: GRADIENT.bard,
394
- sunset: GRADIENT.sunset,
395
- ocean: GRADIENT.ocean,
396
- ice: GRADIENT.ice,
397
- fire: GRADIENT.fire,
398
- forest: GRADIENT.forest,
399
- };
400
389
  function hexToRgb(hex) {
401
390
  const n = parseInt(hex.slice(1), 16);
402
391
  return { r: n >> 16, g: (n >> 8) & 255, b: n & 255 };
@@ -407,8 +396,12 @@ var lerpRgb = (a, b, t) => ({
407
396
  b: Math.round(a.b + (b.b - a.b) * t),
408
397
  });
409
398
  function getPaletteColor(palette, position, timeOffset = 0) {
410
- if (palette.length === 0) return { r: 255, g: 255, b: 255 };
411
- if (palette.length === 1) return hexToRgb(palette[0]);
399
+ if (palette.length === 0) {
400
+ return { r: 255, g: 255, b: 255 };
401
+ }
402
+ if (palette.length === 1) {
403
+ return hexToRgb(palette[0]);
404
+ }
412
405
  const cyclePos = (position + timeOffset) % 1;
413
406
  const scaled = cyclePos * palette.length;
414
407
  const idx = Math.floor(scaled);
@@ -417,11 +410,120 @@ function getPaletteColor(palette, position, timeOffset = 0) {
417
410
  const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
418
411
  return lerpRgb(c1, c2, t);
419
412
  }
420
- function resolvePalette(palette, trailStyle) {
421
- if (Array.isArray(palette)) return palette;
422
- if (palette && palette in PRESETS) return PRESETS[palette];
423
- return trailStyle === "gradient-animated" ? GRADIENT.bard : GRADIENT.ice;
413
+ var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
414
+ var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
415
+ var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
416
+ "trailColor",
417
+ "headColor",
418
+ "skeletonColor",
419
+ "trailStyle",
420
+ ]);
421
+ function validateRenderOptions(partial) {
422
+ for (const key of Object.keys(partial)) {
423
+ if (!RENDER_OPTION_KEYS.has(key)) {
424
+ throw new TypeError(`[sarmal] setRenderOptions: unknown key "${key}"`);
425
+ }
426
+ }
427
+ if (partial.trailColor !== void 0) {
428
+ assertTrailColor(partial.trailColor);
429
+ }
430
+ if (partial.headColor !== void 0) {
431
+ assertHeadColor(partial.headColor);
432
+ }
433
+ if (partial.skeletonColor !== void 0) {
434
+ assertSkeletonColor(partial.skeletonColor);
435
+ }
436
+ if (partial.trailStyle !== void 0) {
437
+ assertTrailStyle(partial.trailStyle);
438
+ }
439
+ }
440
+ function assertTrailColor(value) {
441
+ if (typeof value === "string") {
442
+ if (!HEX_COLOR_RE.test(value)) {
443
+ throw new TypeError(
444
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`,
445
+ );
446
+ }
447
+ return;
448
+ }
449
+ if (Array.isArray(value)) {
450
+ if (value.length < 2) {
451
+ throw new RangeError(
452
+ `[sarmal] setRenderOptions: trailColor array must have at least 2 entries, got ${value.length}`,
453
+ );
454
+ }
455
+ for (let i = 0; i < value.length; i++) {
456
+ const entry = value[i];
457
+ if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
458
+ throw new TypeError(
459
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`,
460
+ );
461
+ }
462
+ }
463
+ return;
464
+ }
465
+ throw new TypeError(
466
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`,
467
+ );
468
+ }
469
+ function assertHeadColor(value) {
470
+ if (value === null) {
471
+ return;
472
+ }
473
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
474
+ throw new TypeError(
475
+ `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`,
476
+ );
477
+ }
478
+ }
479
+ function assertSkeletonColor(value) {
480
+ if (value === "transparent") {
481
+ return;
482
+ }
483
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
484
+ throw new TypeError(
485
+ `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`,
486
+ );
487
+ }
488
+ }
489
+ function assertTrailStyle(value) {
490
+ if (!TRAIL_STYLES.includes(value)) {
491
+ throw new RangeError(
492
+ `[sarmal] setRenderOptions: trailStyle must be one of "default", "gradient-static", "gradient-animated", got ${JSON.stringify(value)}`,
493
+ );
494
+ }
495
+ }
496
+ function resolveTrailMainColor(trailColor) {
497
+ return typeof trailColor === "string" ? trailColor : trailColor[0];
498
+ }
499
+ function resolveTrailPalette(trailColor) {
500
+ return typeof trailColor === "string" ? [trailColor] : trailColor;
501
+ }
502
+ function resolveHeadColor(trailColor, trailStyle) {
503
+ if (trailStyle === "default") {
504
+ return resolveTrailMainColor(trailColor);
505
+ }
506
+ const palette = resolveTrailPalette(trailColor);
507
+ const last = palette[palette.length - 1];
508
+ const { r, g, b } = hexToRgb(last);
509
+ return `rgb(${r},${g},${b})`;
510
+ }
511
+ function warnIfTrailColorMismatch(trailColor, trailStyle) {
512
+ if (trailStyle === "default" && Array.isArray(trailColor)) {
513
+ console.warn(
514
+ '[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.',
515
+ );
516
+ return;
517
+ }
518
+ if (trailStyle !== "default" && typeof trailColor === "string") {
519
+ console.warn(
520
+ `[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.`,
521
+ );
522
+ }
424
523
  }
524
+
525
+ // src/renderer.ts
526
+ var WHITE_HEX = "#ffffff";
425
527
  function hexToRgbComponents(hex) {
426
528
  const n = parseInt(hex.slice(1), 16);
427
529
  return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
@@ -439,22 +541,14 @@ function createRenderer(options) {
439
541
  }
440
542
  const ctx = canvas.getContext("2d");
441
543
  const engine = options.engine;
442
- const trailStyle = options.trailStyle ?? "default";
443
- const trailColor = options.trailColor ?? "#ffffff";
444
- const palette = resolvePalette(options.palette, trailStyle);
445
- function defaultHeadColor() {
446
- if (trailStyle !== "default") {
447
- const { r, g, b } = getPaletteColor(palette, 1);
448
- return `rgb(${r},${g},${b})`;
449
- }
450
- return trailColor;
451
- }
452
- const opts = {
453
- skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,
454
- trailColor,
455
- headColor: options.headColor ?? defaultHeadColor(),
456
- };
457
- const trailRgb = hexToRgbComponents(opts.trailColor);
544
+ let trailStyle = options.trailStyle ?? "default";
545
+ let trailColor = options.trailColor ?? WHITE_HEX;
546
+ let skeletonColor = options.skeletonColor ?? WHITE_HEX;
547
+ let userHeadColor = options.headColor ?? null;
548
+ let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
549
+ let trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
550
+ let trailPalette = resolveTrailPalette(trailColor);
551
+ warnIfTrailColorMismatch(trailColor, trailStyle);
458
552
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
459
553
  function setupCanvas() {
460
554
  const rect = canvas.getBoundingClientRect();
@@ -489,11 +583,13 @@ function createRenderer(options) {
489
583
  }
490
584
  }
491
585
  function buildSkeletonCanvas() {
492
- if (skeleton.length < 2) return;
586
+ if (skeleton.length < 2) {
587
+ return;
588
+ }
493
589
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
494
590
  const skeletonCtx = skeletonCanvas.getContext("2d");
495
591
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
496
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
592
+ skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
497
593
  skeletonCtx.lineWidth = 1.5;
498
594
  skeletonCtx.beginPath();
499
595
  const first = skeleton[0];
@@ -505,8 +601,10 @@ function createRenderer(options) {
505
601
  skeletonCtx.stroke();
506
602
  }
507
603
  function drawSkeletonPath(pts, opacity) {
508
- if (pts.length < 2) return;
509
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${opacity})`;
604
+ if (pts.length < 2) {
605
+ return;
606
+ }
607
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
510
608
  ctx.lineWidth = 1.5;
511
609
  ctx.beginPath();
512
610
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -516,7 +614,7 @@ function createRenderer(options) {
516
614
  ctx.stroke();
517
615
  }
518
616
  function drawSkeleton() {
519
- if (opts.skeletonColor === "transparent") {
617
+ if (skeletonColor === "transparent") {
520
618
  return;
521
619
  }
522
620
  if (engine.morphAlpha !== null) {
@@ -527,7 +625,7 @@ function createRenderer(options) {
527
625
  if (skeleton.length < 2) {
528
626
  return;
529
627
  }
530
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
628
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
531
629
  ctx.lineWidth = 1.5;
532
630
  ctx.beginPath();
533
631
  const first = skeleton[0];
@@ -556,10 +654,10 @@ function createRenderer(options) {
556
654
  toY,
557
655
  );
558
656
  if (trailStyle === "default") {
559
- ctx.fillStyle = `rgba(${trailRgb},${opacity})`;
657
+ ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
560
658
  } else {
561
659
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
562
- const color = getPaletteColor(palette, progress, timeOffset);
660
+ const color = getPaletteColor(trailPalette, progress, timeOffset);
563
661
  ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
564
662
  }
565
663
  ctx.beginPath();
@@ -579,7 +677,7 @@ function createRenderer(options) {
579
677
  const y = head.y * scale + offsetY;
580
678
  const r =
581
679
  options.headRadius ?? Math.max(2, 3 * Math.sqrt(Math.min(logicalWidth, logicalHeight) / 160));
582
- ctx.fillStyle = opts.headColor;
680
+ ctx.fillStyle = headColor;
583
681
  ctx.beginPath();
584
682
  ctx.arc(x, y, r, 0, Math.PI * 2);
585
683
  ctx.fill();
@@ -680,6 +778,34 @@ function createRenderer(options) {
680
778
  morphResolve = resolve;
681
779
  });
682
780
  },
781
+ setRenderOptions(partial) {
782
+ validateRenderOptions(partial);
783
+ if (partial.trailColor !== void 0) {
784
+ trailColor = partial.trailColor;
785
+ trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
786
+ trailPalette = resolveTrailPalette(trailColor);
787
+ }
788
+ if (partial.skeletonColor !== void 0) {
789
+ skeletonColor = partial.skeletonColor;
790
+ if (skeletonColor !== "transparent" && !engine.isLiveSkeleton) {
791
+ buildSkeletonCanvas();
792
+ }
793
+ }
794
+ if (partial.trailStyle !== void 0) {
795
+ trailStyle = partial.trailStyle;
796
+ }
797
+ if (partial.headColor !== void 0) {
798
+ userHeadColor = partial.headColor;
799
+ }
800
+ if (userHeadColor === null) {
801
+ headColor = resolveHeadColor(trailColor, trailStyle);
802
+ } else {
803
+ headColor = userHeadColor;
804
+ }
805
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
806
+ warnIfTrailColorMismatch(trailColor, trailStyle);
807
+ }
808
+ },
683
809
  };
684
810
  if (shouldAutoStart) {
685
811
  instance.play();
@@ -715,13 +841,15 @@ function el(tag) {
715
841
  }
716
842
  function createSVGRenderer(options) {
717
843
  const { container, engine } = options;
718
- const trailColor = options.trailColor ?? "#ffffff";
719
- const opts = {
720
- skeletonColor: options.skeletonColor ?? "#ffffff",
721
- trailColor,
722
- headColor: options.headColor ?? trailColor,
723
- ariaLabel: options.ariaLabel ?? "Loading",
724
- };
844
+ let trailStyle = options.trailStyle ?? "default";
845
+ let trailColor = options.trailColor ?? "#ffffff";
846
+ let skeletonColor = options.skeletonColor ?? "#ffffff";
847
+ let userHeadColor = options.headColor ?? null;
848
+ let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
849
+ let trailSolid = resolveTrailMainColor(trailColor);
850
+ let trailPalette = resolveTrailPalette(trailColor);
851
+ const ariaLabel = options.ariaLabel ?? "Loading";
852
+ warnIfTrailColorMismatch(trailColor, trailStyle);
725
853
  const rect = container.getBoundingClientRect();
726
854
  const width = rect.width || 200;
727
855
  const height = rect.height || 200;
@@ -732,25 +860,29 @@ function createSVGRenderer(options) {
732
860
  svg.setAttribute("height", String(height));
733
861
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
734
862
  svg.setAttribute("role", "img");
735
- svg.setAttribute("aria-label", opts.ariaLabel);
863
+ svg.setAttribute("aria-label", ariaLabel);
736
864
  const titleEl = el("title");
737
- titleEl.textContent = opts.ariaLabel;
865
+ titleEl.textContent = ariaLabel;
738
866
  svg.appendChild(titleEl);
739
867
  const skeletonPath = el("path");
868
+ skeletonPath.setAttribute("data-sarmal-role", "skeleton");
740
869
  skeletonPath.setAttribute("fill", "none");
741
- skeletonPath.setAttribute("stroke", opts.skeletonColor);
870
+ skeletonPath.setAttribute("stroke", skeletonColor);
742
871
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
743
872
  skeletonPath.setAttribute("stroke-width", "1.5");
873
+ if (skeletonColor === "transparent") {
874
+ skeletonPath.setAttribute("visibility", "hidden");
875
+ }
744
876
  svg.appendChild(skeletonPath);
745
877
  const skeletonPathA = el("path");
746
878
  skeletonPathA.setAttribute("fill", "none");
747
- skeletonPathA.setAttribute("stroke", opts.skeletonColor);
879
+ skeletonPathA.setAttribute("stroke", skeletonColor);
748
880
  skeletonPathA.setAttribute("stroke-width", "1.5");
749
881
  skeletonPathA.setAttribute("visibility", "hidden");
750
882
  svg.appendChild(skeletonPathA);
751
883
  const skeletonPathB = el("path");
752
884
  skeletonPathB.setAttribute("fill", "none");
753
- skeletonPathB.setAttribute("stroke", opts.skeletonColor);
885
+ skeletonPathB.setAttribute("stroke", skeletonColor);
754
886
  skeletonPathB.setAttribute("stroke-width", "1.5");
755
887
  skeletonPathB.setAttribute("visibility", "hidden");
756
888
  svg.appendChild(skeletonPathB);
@@ -759,15 +891,17 @@ function createSVGRenderer(options) {
759
891
  const trailPaths = [];
760
892
  for (let i = 0; i < MAX_TRAIL_SEGMENTS; i++) {
761
893
  const path = el("path");
762
- path.setAttribute("fill", opts.trailColor);
894
+ path.setAttribute("fill", trailSolid);
763
895
  svg.appendChild(path);
764
896
  trailPaths.push(path);
765
897
  }
766
898
  const headCircle = el("circle");
767
- headCircle.setAttribute("fill", opts.headColor);
899
+ headCircle.setAttribute("data-sarmal-role", "head");
900
+ headCircle.setAttribute("fill", headColor);
768
901
  headCircle.setAttribute("r", String(headRadius));
769
902
  svg.appendChild(headCircle);
770
903
  container.appendChild(svg);
904
+ let gradientAnimTime = 0;
771
905
  let scale = 1;
772
906
  let offsetX = 0;
773
907
  let offsetY = 0;
@@ -801,7 +935,7 @@ function createSVGRenderer(options) {
801
935
  return;
802
936
  }
803
937
  for (let i = 0; i < trailCount - 1; i++) {
804
- const { l0x, l0y, r0x, r0y, l1x, l1y, r1x, r1y, opacity } = computeTrailQuad(
938
+ const { l0x, l0y, r0x, r0y, l1x, l1y, r1x, r1y, opacity, progress } = computeTrailQuad(
805
939
  trail,
806
940
  i,
807
941
  trailCount,
@@ -811,6 +945,11 @@ function createSVGRenderer(options) {
811
945
  const d = `M${l0x.toFixed(2)} ${l0y.toFixed(2)} L${l1x.toFixed(2)} ${l1y.toFixed(2)} L${r1x.toFixed(2)} ${r1y.toFixed(2)} L${r0x.toFixed(2)} ${r0y.toFixed(2)} Z`;
812
946
  trailPaths[i].setAttribute("d", d);
813
947
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
948
+ if (trailStyle !== "default") {
949
+ const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
950
+ const { r, g, b } = getPaletteColor(trailPalette, progress, timeOffset);
951
+ trailPaths[i].setAttribute("fill", `rgb(${r},${g},${b})`);
952
+ }
814
953
  }
815
954
  for (let i = trailCount - 1; i < trailPaths.length; i++) {
816
955
  trailPaths[i].setAttribute("d", "");
@@ -835,6 +974,9 @@ function createSVGRenderer(options) {
835
974
  let morphTarget = null;
836
975
  let morphAlpha = 0;
837
976
  function renderFrame(deltaTime) {
977
+ if (trailStyle === "gradient-animated") {
978
+ gradientAnimTime += deltaTime * 1e3;
979
+ }
838
980
  if (engine.morphAlpha !== null) {
839
981
  morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
840
982
  engine.setMorphAlpha(morphAlpha);
@@ -940,6 +1082,51 @@ function createSVGRenderer(options) {
940
1082
  morphResolve = resolve;
941
1083
  });
942
1084
  },
1085
+ setRenderOptions(partial) {
1086
+ validateRenderOptions(partial);
1087
+ const prevTrailStyle = trailStyle;
1088
+ if (partial.trailColor !== void 0) {
1089
+ trailColor = partial.trailColor;
1090
+ trailSolid = resolveTrailMainColor(trailColor);
1091
+ trailPalette = resolveTrailPalette(trailColor);
1092
+ if (trailStyle === "default") {
1093
+ for (const p of trailPaths) {
1094
+ p.setAttribute("fill", trailSolid);
1095
+ }
1096
+ }
1097
+ }
1098
+ if (partial.skeletonColor !== void 0) {
1099
+ skeletonColor = partial.skeletonColor;
1100
+ if (skeletonColor === "transparent") {
1101
+ skeletonPath.setAttribute("visibility", "hidden");
1102
+ } else {
1103
+ skeletonPath.setAttribute("stroke", skeletonColor);
1104
+ skeletonPath.removeAttribute("visibility");
1105
+ skeletonPathA.setAttribute("stroke", skeletonColor);
1106
+ skeletonPathB.setAttribute("stroke", skeletonColor);
1107
+ }
1108
+ }
1109
+ if (partial.trailStyle !== void 0) {
1110
+ trailStyle = partial.trailStyle;
1111
+ if (prevTrailStyle !== "default" && trailStyle === "default") {
1112
+ for (const p of trailPaths) {
1113
+ p.setAttribute("fill", trailSolid);
1114
+ }
1115
+ }
1116
+ }
1117
+ if (partial.headColor !== void 0) {
1118
+ userHeadColor = partial.headColor;
1119
+ }
1120
+ if (userHeadColor === null) {
1121
+ headColor = resolveHeadColor(trailColor, trailStyle);
1122
+ } else {
1123
+ headColor = userHeadColor;
1124
+ }
1125
+ headCircle.setAttribute("fill", headColor);
1126
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1127
+ warnIfTrailColorMismatch(trailColor, trailStyle);
1128
+ }
1129
+ },
943
1130
  };
944
1131
  if (shouldAutoStart) {
945
1132
  instance.play();
@@ -1164,6 +1351,7 @@ exports.epitrochoid7 = epitrochoid7;
1164
1351
  exports.lame = lame;
1165
1352
  exports.lissajous32 = lissajous32;
1166
1353
  exports.lissajous43 = lissajous43;
1354
+ exports.palettes = palettes;
1167
1355
  exports.rose3 = rose3;
1168
1356
  exports.rose5 = rose5;
1169
1357
  //# sourceMappingURL=index.cjs.map