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