@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.
@@ -378,25 +378,6 @@ 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 = {
385
- bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
386
- sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
387
- ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
388
- ice: ["#1e3a8a", "#67e8f9"],
389
- fire: ["#7f1d1d", "#fbbf24"],
390
- forest: ["#14532d", "#86efac"],
391
- };
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
381
  function hexToRgb(hex) {
401
382
  const n = parseInt(hex.slice(1), 16);
402
383
  return { r: n >> 16, g: (n >> 8) & 255, b: n & 255 };
@@ -407,8 +388,12 @@ var lerpRgb = (a, b, t) => ({
407
388
  b: Math.round(a.b + (b.b - a.b) * t),
408
389
  });
409
390
  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]);
391
+ if (palette.length === 0) {
392
+ return { r: 255, g: 255, b: 255 };
393
+ }
394
+ if (palette.length === 1) {
395
+ return hexToRgb(palette[0]);
396
+ }
412
397
  const cyclePos = (position + timeOffset) % 1;
413
398
  const scaled = cyclePos * palette.length;
414
399
  const idx = Math.floor(scaled);
@@ -417,11 +402,120 @@ function getPaletteColor(palette, position, timeOffset = 0) {
417
402
  const c2 = hexToRgb(palette[(idx + 1) % palette.length]);
418
403
  return lerpRgb(c1, c2, t);
419
404
  }
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;
405
+ var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
406
+ var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
407
+ var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
408
+ "trailColor",
409
+ "headColor",
410
+ "skeletonColor",
411
+ "trailStyle",
412
+ ]);
413
+ function validateRenderOptions(partial) {
414
+ for (const key of Object.keys(partial)) {
415
+ if (!RENDER_OPTION_KEYS.has(key)) {
416
+ throw new TypeError(`[sarmal] setRenderOptions: unknown key "${key}"`);
417
+ }
418
+ }
419
+ if (partial.trailColor !== void 0) {
420
+ assertTrailColor(partial.trailColor);
421
+ }
422
+ if (partial.headColor !== void 0) {
423
+ assertHeadColor(partial.headColor);
424
+ }
425
+ if (partial.skeletonColor !== void 0) {
426
+ assertSkeletonColor(partial.skeletonColor);
427
+ }
428
+ if (partial.trailStyle !== void 0) {
429
+ assertTrailStyle(partial.trailStyle);
430
+ }
431
+ }
432
+ function assertTrailColor(value) {
433
+ if (typeof value === "string") {
434
+ if (!HEX_COLOR_RE.test(value)) {
435
+ throw new TypeError(
436
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string, got "${value}"`,
437
+ );
438
+ }
439
+ return;
440
+ }
441
+ if (Array.isArray(value)) {
442
+ if (value.length < 2) {
443
+ throw new RangeError(
444
+ `[sarmal] setRenderOptions: trailColor array must have at least 2 entries, got ${value.length}`,
445
+ );
446
+ }
447
+ for (let i = 0; i < value.length; i++) {
448
+ const entry = value[i];
449
+ if (typeof entry !== "string" || !HEX_COLOR_RE.test(entry)) {
450
+ throw new TypeError(
451
+ `[sarmal] setRenderOptions: trailColor[${i}] must be a 6-digit hex string, got ${JSON.stringify(entry)}`,
452
+ );
453
+ }
454
+ }
455
+ return;
456
+ }
457
+ throw new TypeError(
458
+ `[sarmal] setRenderOptions: trailColor must be a 6-digit hex string or an array of hex strings, got ${JSON.stringify(value)}`,
459
+ );
460
+ }
461
+ function assertHeadColor(value) {
462
+ if (value === null) {
463
+ return;
464
+ }
465
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
466
+ throw new TypeError(
467
+ `[sarmal] setRenderOptions: headColor must be a 6-digit hex string or null, got ${JSON.stringify(value)}`,
468
+ );
469
+ }
470
+ }
471
+ function assertSkeletonColor(value) {
472
+ if (value === "transparent") {
473
+ return;
474
+ }
475
+ if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) {
476
+ throw new TypeError(
477
+ `[sarmal] setRenderOptions: skeletonColor must be a 6-digit hex string or "transparent", got ${JSON.stringify(value)}`,
478
+ );
479
+ }
424
480
  }
481
+ function assertTrailStyle(value) {
482
+ if (!TRAIL_STYLES.includes(value)) {
483
+ throw new RangeError(
484
+ `[sarmal] setRenderOptions: trailStyle must be one of "default", "gradient-static", "gradient-animated", got ${JSON.stringify(value)}`,
485
+ );
486
+ }
487
+ }
488
+ function resolveTrailMainColor(trailColor) {
489
+ return typeof trailColor === "string" ? trailColor : trailColor[0];
490
+ }
491
+ function resolveTrailPalette(trailColor) {
492
+ return typeof trailColor === "string" ? [trailColor] : trailColor;
493
+ }
494
+ function resolveHeadColor(trailColor, trailStyle) {
495
+ if (trailStyle === "default") {
496
+ return resolveTrailMainColor(trailColor);
497
+ }
498
+ const palette = resolveTrailPalette(trailColor);
499
+ const last = palette[palette.length - 1];
500
+ const { r, g, b } = hexToRgb(last);
501
+ return `rgb(${r},${g},${b})`;
502
+ }
503
+ function warnIfTrailColorMismatch(trailColor, trailStyle) {
504
+ if (trailStyle === "default" && Array.isArray(trailColor)) {
505
+ console.warn(
506
+ '[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.',
507
+ );
508
+ return;
509
+ }
510
+ if (trailStyle !== "default" && typeof trailColor === "string") {
511
+ console.warn(
512
+ `[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.`,
513
+ );
514
+ }
515
+ }
516
+
517
+ // src/renderer.ts
518
+ var WHITE_HEX = "#ffffff";
425
519
  function hexToRgbComponents(hex) {
426
520
  const n = parseInt(hex.slice(1), 16);
427
521
  return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
@@ -439,22 +533,14 @@ function createRenderer(options) {
439
533
  }
440
534
  const ctx = canvas.getContext("2d");
441
535
  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);
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);
458
544
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
459
545
  function setupCanvas() {
460
546
  const rect = canvas.getBoundingClientRect();
@@ -489,11 +575,13 @@ function createRenderer(options) {
489
575
  }
490
576
  }
491
577
  function buildSkeletonCanvas() {
492
- if (skeleton.length < 2) return;
578
+ if (skeleton.length < 2) {
579
+ return;
580
+ }
493
581
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
494
582
  const skeletonCtx = skeletonCanvas.getContext("2d");
495
583
  skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
496
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
584
+ skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
497
585
  skeletonCtx.lineWidth = 1.5;
498
586
  skeletonCtx.beginPath();
499
587
  const first = skeleton[0];
@@ -505,8 +593,10 @@ function createRenderer(options) {
505
593
  skeletonCtx.stroke();
506
594
  }
507
595
  function drawSkeletonPath(pts, opacity) {
508
- if (pts.length < 2) return;
509
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${opacity})`;
596
+ if (pts.length < 2) {
597
+ return;
598
+ }
599
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${opacity})`;
510
600
  ctx.lineWidth = 1.5;
511
601
  ctx.beginPath();
512
602
  ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
@@ -516,7 +606,7 @@ function createRenderer(options) {
516
606
  ctx.stroke();
517
607
  }
518
608
  function drawSkeleton() {
519
- if (opts.skeletonColor === "transparent") {
609
+ if (skeletonColor === "transparent") {
520
610
  return;
521
611
  }
522
612
  if (engine.morphAlpha !== null) {
@@ -527,7 +617,7 @@ function createRenderer(options) {
527
617
  if (skeleton.length < 2) {
528
618
  return;
529
619
  }
530
- ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
620
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
531
621
  ctx.lineWidth = 1.5;
532
622
  ctx.beginPath();
533
623
  const first = skeleton[0];
@@ -556,10 +646,10 @@ function createRenderer(options) {
556
646
  toY,
557
647
  );
558
648
  if (trailStyle === "default") {
559
- ctx.fillStyle = `rgba(${trailRgb},${opacity})`;
649
+ ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
560
650
  } else {
561
651
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
562
- const color = getPaletteColor(palette, progress, timeOffset);
652
+ const color = getPaletteColor(trailPalette, progress, timeOffset);
563
653
  ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
564
654
  }
565
655
  ctx.beginPath();
@@ -579,7 +669,7 @@ function createRenderer(options) {
579
669
  const y = head.y * scale + offsetY;
580
670
  const r =
581
671
  options.headRadius ?? Math.max(2, 3 * Math.sqrt(Math.min(logicalWidth, logicalHeight) / 160));
582
- ctx.fillStyle = opts.headColor;
672
+ ctx.fillStyle = headColor;
583
673
  ctx.beginPath();
584
674
  ctx.arc(x, y, r, 0, Math.PI * 2);
585
675
  ctx.fill();
@@ -680,6 +770,34 @@ function createRenderer(options) {
680
770
  morphResolve = resolve;
681
771
  });
682
772
  },
773
+ setRenderOptions(partial) {
774
+ validateRenderOptions(partial);
775
+ if (partial.trailColor !== void 0) {
776
+ trailColor = partial.trailColor;
777
+ trailSolidRgb = hexToRgbComponents(resolveTrailMainColor(trailColor));
778
+ trailPalette = resolveTrailPalette(trailColor);
779
+ }
780
+ if (partial.skeletonColor !== void 0) {
781
+ skeletonColor = partial.skeletonColor;
782
+ if (skeletonColor !== "transparent" && !engine.isLiveSkeleton) {
783
+ buildSkeletonCanvas();
784
+ }
785
+ }
786
+ if (partial.trailStyle !== void 0) {
787
+ trailStyle = partial.trailStyle;
788
+ }
789
+ if (partial.headColor !== void 0) {
790
+ userHeadColor = partial.headColor;
791
+ }
792
+ if (userHeadColor === null) {
793
+ headColor = resolveHeadColor(trailColor, trailStyle);
794
+ } else {
795
+ headColor = userHeadColor;
796
+ }
797
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
798
+ warnIfTrailColorMismatch(trailColor, trailStyle);
799
+ }
800
+ },
683
801
  };
684
802
  if (shouldAutoStart) {
685
803
  instance.play();
@@ -886,7 +1004,7 @@ function createSarmal(canvas, curveDef, options) {
886
1004
  }
887
1005
 
888
1006
  // src/auto-init.ts
889
- function parsePalette(value) {
1007
+ function parseTrailColor(value) {
890
1008
  try {
891
1009
  const parsed = JSON.parse(value);
892
1010
  if (Array.isArray(parsed)) {
@@ -907,7 +1025,9 @@ function init() {
907
1025
  return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
908
1026
  }
909
1027
  createSarmal(canvas, curveDef, {
910
- ...(canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor }),
1028
+ ...(canvas.dataset.trailColor && {
1029
+ trailColor: parseTrailColor(canvas.dataset.trailColor),
1030
+ }),
911
1031
  ...(canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor }),
912
1032
  ...(canvas.dataset.headColor && { headColor: canvas.dataset.headColor }),
913
1033
  ...(canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) }),
@@ -915,7 +1035,6 @@ function init() {
915
1035
  ...(canvas.dataset.trailStyle && {
916
1036
  trailStyle: canvas.dataset.trailStyle,
917
1037
  }),
918
- ...(canvas.dataset.palette && { palette: parsePalette(canvas.dataset.palette) }),
919
1038
  });
920
1039
  });
921
1040
  }