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