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