@sarmal/core 0.31.0 → 0.34.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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/dist/auto-init.cjs +42 -4
  3. package/dist/auto-init.cjs.map +1 -1
  4. package/dist/auto-init.js +42 -4
  5. package/dist/auto-init.js.map +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/curves/artemis2.d.cts +1 -1
  8. package/dist/curves/artemis2.d.ts +1 -1
  9. package/dist/curves/astroid.d.cts +1 -1
  10. package/dist/curves/astroid.d.ts +1 -1
  11. package/dist/curves/deltoid.d.cts +1 -1
  12. package/dist/curves/deltoid.d.ts +1 -1
  13. package/dist/curves/epicycloid3.d.cts +1 -1
  14. package/dist/curves/epicycloid3.d.ts +1 -1
  15. package/dist/curves/epitrochoid7.d.cts +1 -1
  16. package/dist/curves/epitrochoid7.d.ts +1 -1
  17. package/dist/curves/index.d.cts +1 -1
  18. package/dist/curves/index.d.ts +1 -1
  19. package/dist/curves/lame.d.cts +1 -1
  20. package/dist/curves/lame.d.ts +1 -1
  21. package/dist/curves/lissajous32.d.cts +1 -1
  22. package/dist/curves/lissajous32.d.ts +1 -1
  23. package/dist/curves/lissajous43.d.cts +1 -1
  24. package/dist/curves/lissajous43.d.ts +1 -1
  25. package/dist/curves/rose3.d.cts +1 -1
  26. package/dist/curves/rose3.d.ts +1 -1
  27. package/dist/curves/rose5.d.cts +1 -1
  28. package/dist/curves/rose5.d.ts +1 -1
  29. package/dist/curves/rose52.d.cts +1 -1
  30. package/dist/curves/rose52.d.ts +1 -1
  31. package/dist/curves/star.d.cts +1 -1
  32. package/dist/curves/star.d.ts +1 -1
  33. package/dist/curves/star4.d.cts +1 -1
  34. package/dist/curves/star4.d.ts +1 -1
  35. package/dist/curves/star7.d.cts +1 -1
  36. package/dist/curves/star7.d.ts +1 -1
  37. package/dist/index.cjs +375 -7
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +80 -4
  40. package/dist/index.d.ts +80 -4
  41. package/dist/index.js +375 -8
  42. package/dist/index.js.map +1 -1
  43. package/dist/{renderer-shared-jqw_Q1WO.d.cts → renderer-shared-C3KCEABq.d.cts} +8 -4
  44. package/dist/{renderer-shared-OR--cv-t.d.ts → renderer-shared-DyOI68gd.d.ts} +8 -4
  45. package/dist/terminal.cjs.map +1 -1
  46. package/dist/terminal.d.cts +2 -2
  47. package/dist/terminal.d.ts +2 -2
  48. package/dist/terminal.js.map +1 -1
  49. package/dist/{types-zbxUgcmZ.d.cts → types-_f27GDkU.d.cts} +30 -9
  50. package/dist/{types-zbxUgcmZ.d.ts → types-_f27GDkU.d.ts} +30 -9
  51. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * Lissajous curve with frequency ratio 3:2
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * Lissajous curve with frequency ratio 4:3
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * Lissajous curve with frequency ratio 4:3
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * Rose curve with 3 petals
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * Rose curve with 3 petals
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * Rose curve with 5 petals
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * Rose curve with 5 petals
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * Rose curve with n=5/2 that traces 5 petals over two full revolutions
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * Rose curve with n=5/2 that traces 5 petals over two full revolutions
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * 5-pointed star based on Fourier harmonics.
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * 5-pointed star based on Fourier harmonics.
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * 4-pointed star based on Fourier harmonics.
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * 4-pointed star based on Fourier harmonics.
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.cjs';
1
+ import { C as CurveDef } from '../types-_f27GDkU.cjs';
2
2
 
3
3
  /**
4
4
  * 7-pointed star based on Fourier harmonics.
@@ -1,4 +1,4 @@
1
- import { C as CurveDef } from '../types-zbxUgcmZ.js';
1
+ import { C as CurveDef } from '../types-_f27GDkU.js';
2
2
 
3
3
  /**
4
4
  * 7-pointed star based on Fourier harmonics.
package/dist/index.cjs CHANGED
@@ -375,11 +375,15 @@ function enginePassthroughs(engine) {
375
375
  }
376
376
  var palettes = {
377
377
  bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
378
- sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
378
+ carnival: ["#ff6b6b", "#4ecdc4", "#ffe66d"],
379
379
  ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
380
+ sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
380
381
  ice: ["#1e3a8a", "#67e8f9"],
381
- fire: ["#7f1d1d", "#fbbf24"],
382
- forest: ["#14532d", "#86efac"]
382
+ rocketpop: ["#08b8cd", "#ffffff", "#ff001f"],
383
+ neon: ["#00e5ff", "#7c3aed", "#e040fb"],
384
+ vaporwave: ["#ff71ce", "#01cdfe", "#b967ff"],
385
+ pastel: ["#c4b5fd", "#fbcfe8", "#bae6fd"],
386
+ sakura: ["#fff1f2", "#fda4af", "#fb7185"]
383
387
  };
384
388
  function hexToRgb(hex) {
385
389
  const n = parseInt(hex.slice(1), 16);
@@ -516,13 +520,28 @@ function getPaletteColor(palette, position, timeOffset = 0) {
516
520
  return lerpOklab(c1, c2, t);
517
521
  }
518
522
  var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
523
+ var BASE_RENDER_OPTION_KEYS = /* @__PURE__ */ new Set(["trailColor", "trailStyle"]);
519
524
  var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
520
525
  "trailColor",
521
526
  "headColor",
522
527
  "skeletonColor",
523
528
  "trailStyle",
524
- "headRadius"
529
+ "headRadius",
530
+ "trailWidth"
525
531
  ]);
532
+ function validateBaseRenderOptions(partial) {
533
+ for (const key of Object.keys(partial)) {
534
+ if (!BASE_RENDER_OPTION_KEYS.has(key)) {
535
+ throw new TypeError(`[sarmal] setRenderOptions: unsupported key "${key}" for this renderer`);
536
+ }
537
+ }
538
+ if (partial.trailColor !== void 0) {
539
+ assertTrailColor(partial.trailColor);
540
+ }
541
+ if (partial.trailStyle !== void 0) {
542
+ assertTrailStyle(partial.trailStyle);
543
+ }
544
+ }
526
545
  function validateRenderOptions(partial) {
527
546
  for (const key of Object.keys(partial)) {
528
547
  if (!RENDER_OPTION_KEYS.has(key)) {
@@ -544,6 +563,9 @@ function validateRenderOptions(partial) {
544
563
  if (partial.headRadius !== void 0) {
545
564
  assertHeadRadius(partial.headRadius);
546
565
  }
566
+ if (partial.trailWidth !== void 0) {
567
+ assertTrailWidth(partial.trailWidth);
568
+ }
547
569
  }
548
570
  function assertTrailColor(value) {
549
571
  if (typeof value === "string") {
@@ -613,6 +635,18 @@ function assertHeadRadius(value) {
613
635
  );
614
636
  }
615
637
  }
638
+ function assertTrailWidth(value) {
639
+ if (typeof value !== "number") {
640
+ throw new TypeError(
641
+ `[sarmal] setRenderOptions: trailWidth must be a number, got ${JSON.stringify(value)}`
642
+ );
643
+ }
644
+ if (!Number.isFinite(value) || value <= 0) {
645
+ throw new TypeError(
646
+ `[sarmal] setRenderOptions: trailWidth must be a finite positive number, got ${value}`
647
+ );
648
+ }
649
+ }
616
650
  function resolveTrailMainColor(trailColor) {
617
651
  return typeof trailColor === "string" ? trailColor : trailColor[0];
618
652
  }
@@ -681,7 +715,14 @@ function createRenderer(options) {
681
715
  setupCanvas();
682
716
  let logicalWidth = canvas.width / dpr;
683
717
  let logicalHeight = canvas.height / dpr;
718
+ if (options.headRadius !== void 0) {
719
+ validateRenderOptions({ headRadius: options.headRadius });
720
+ }
721
+ if (options.trailWidth !== void 0) {
722
+ validateRenderOptions({ trailWidth: options.trailWidth });
723
+ }
684
724
  let headRadius = options.headRadius ?? getHeadDotRadius(logicalWidth, logicalHeight);
725
+ let trailWidth = options.trailWidth ?? 1;
685
726
  let skeleton = [];
686
727
  let skeletonCanvas = null;
687
728
  let trail = [];
@@ -775,7 +816,9 @@ function createRenderer(options) {
775
816
  i,
776
817
  trailCount,
777
818
  toX,
778
- toY
819
+ toY,
820
+ TRAIL_MIN_WIDTH * trailWidth,
821
+ TRAIL_MAX_WIDTH * trailWidth
779
822
  );
780
823
  if (trailStyle === "default") {
781
824
  ctx.fillStyle = `rgba(${trailSolidRgb},${opacity})`;
@@ -933,6 +976,9 @@ function createRenderer(options) {
933
976
  if (partial.headRadius !== void 0) {
934
977
  headRadius = partial.headRadius;
935
978
  }
979
+ if (partial.trailWidth !== void 0) {
980
+ trailWidth = partial.trailWidth;
981
+ }
936
982
  if (userHeadColor === null) {
937
983
  headColor = resolveHeadColor(trailColor, trailStyle);
938
984
  } else {
@@ -1031,7 +1077,14 @@ function createSVGRenderer(options) {
1031
1077
  const svgTrailMinWidth = TRAIL_MIN_WIDTH * viewSize / containerPx;
1032
1078
  const svgTrailMaxWidth = TRAIL_MAX_WIDTH * viewSize / containerPx;
1033
1079
  const svgSkeletonStrokeWidth = String(SKELETON_STROKE_WIDTH_PX * viewSize / containerPx);
1080
+ if (options.headRadius !== void 0) {
1081
+ validateRenderOptions({ headRadius: options.headRadius });
1082
+ }
1083
+ if (options.trailWidth !== void 0) {
1084
+ validateRenderOptions({ trailWidth: options.trailWidth });
1085
+ }
1034
1086
  headRadius = options.headRadius ?? SVG_DEFAULT_HEAD_RADIUS;
1087
+ let trailWidth = options.trailWidth ?? 1;
1035
1088
  container.setAttribute("viewBox", `0 0 ${viewSize} ${viewSize}`);
1036
1089
  container.setAttribute("role", "img");
1037
1090
  container.setAttribute("aria-label", ariaLabel);
@@ -1126,8 +1179,8 @@ function createSVGRenderer(options) {
1126
1179
  trailCount,
1127
1180
  px,
1128
1181
  py,
1129
- svgTrailMinWidth,
1130
- svgTrailMaxWidth
1182
+ svgTrailMinWidth * trailWidth,
1183
+ svgTrailMaxWidth * trailWidth
1131
1184
  );
1132
1185
  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`;
1133
1186
  trailPaths[i].setAttribute("d", d);
@@ -1319,6 +1372,9 @@ function createSVGRenderer(options) {
1319
1372
  headRadius = partial.headRadius;
1320
1373
  headCircle.setAttribute("r", String(headRadius));
1321
1374
  }
1375
+ if (partial.trailWidth !== void 0) {
1376
+ trailWidth = partial.trailWidth;
1377
+ }
1322
1378
  if (userHeadColor === null) {
1323
1379
  headColor = resolveHeadColor(trailColor, trailStyle);
1324
1380
  } else {
@@ -1361,6 +1417,317 @@ function createSarmalSVG(container, curveDef, options) {
1361
1417
  return createSVGRenderer({ container, engine, ...rendererOpts });
1362
1418
  }
1363
1419
 
1420
+ // src/renderer-dot-matrix.ts
1421
+ var NUM_BUCKETS = 8;
1422
+ function createSarmalDotMatrix(canvas, curveDef, options) {
1423
+ const {
1424
+ cols = 32,
1425
+ rows = 32,
1426
+ roundness = 1,
1427
+ trailLength: trailLengthOpt,
1428
+ trailColor: initialColor = "#ffffff",
1429
+ trailStyle: initialTrailStyle = "default",
1430
+ autoStart = true,
1431
+ pauseOnHidden: pauseOnHiddenOpt = true,
1432
+ initialPhase
1433
+ } = options ?? {};
1434
+ const trailLength = trailLengthOpt ?? cols * 3;
1435
+ const engine = createEngine(curveDef, trailLength);
1436
+ if (!canvas.getContext("2d")) {
1437
+ throw new Error("[sarmal] Could not get 2d context from canvas");
1438
+ }
1439
+ const ctx = canvas.getContext("2d");
1440
+ const W = canvas.width;
1441
+ const H = canvas.height;
1442
+ const cellW = W / cols;
1443
+ const cellH = H / rows;
1444
+ const dotR = Math.min(cellW, cellH) * 0.36;
1445
+ let gradientRgb;
1446
+ if (Array.isArray(initialColor)) {
1447
+ validateBaseRenderOptions({ trailColor: initialColor });
1448
+ gradientRgb = initialColor.map(colorToRgb);
1449
+ } else {
1450
+ gradientRgb = null;
1451
+ }
1452
+ let colorRgb = gradientRgb ? gradientRgb[0] : colorToRgb(initialColor);
1453
+ let currentTrailStyle = initialTrailStyle;
1454
+ let animTime = 0;
1455
+ const ANIM_PERIOD = 6;
1456
+ const grid = new Float32Array(cols * rows);
1457
+ let scale = 1;
1458
+ let offsetX = 0;
1459
+ let offsetY = 0;
1460
+ let bgCanvas = null;
1461
+ let animationId = null;
1462
+ let lastTime = 0;
1463
+ let pausedByVisibility = false;
1464
+ let morphResolve = null;
1465
+ let morphReject = null;
1466
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
1467
+ let morphProgress = 0;
1468
+ function buildBgCanvas() {
1469
+ bgCanvas = new OffscreenCanvas(W, H);
1470
+ const bgCtx = bgCanvas.getContext("2d");
1471
+ const bg = gradientRgb ? gradientRgb[0] : colorRgb;
1472
+ bgCtx.fillStyle = `rgba(${bg.r},${bg.g},${bg.b},0.05)`;
1473
+ bgCtx.beginPath();
1474
+ for (let row = 0; row < rows; row++) {
1475
+ for (let col = 0; col < cols; col++) {
1476
+ const cx = (col + 0.5) * cellW;
1477
+ const cy = (row + 0.5) * cellH;
1478
+ bgCtx.roundRect(cx - dotR, cy - dotR, dotR * 2, dotR * 2, roundness * dotR);
1479
+ }
1480
+ }
1481
+ bgCtx.fill();
1482
+ }
1483
+ function sampleGradientRgb(stops, t) {
1484
+ const n = stops.length;
1485
+ const scaled = Math.max(0, Math.min(1, t)) * (n - 1);
1486
+ const i = Math.min(Math.floor(scaled), n - 2);
1487
+ const a = stops[i];
1488
+ const bStop = stops[i + 1];
1489
+ const mix = scaled - i;
1490
+ return {
1491
+ r: Math.round(a.r + (bStop.r - a.r) * mix),
1492
+ g: Math.round(a.g + (bStop.g - a.g) * mix),
1493
+ b: Math.round(a.b + (bStop.b - a.b) * mix)
1494
+ };
1495
+ }
1496
+ function calculateBoundaries(skel) {
1497
+ const b = computeBoundaries(skel, W, H);
1498
+ if (b) {
1499
+ scale = b.scale;
1500
+ offsetX = b.offsetX;
1501
+ offsetY = b.offsetY;
1502
+ }
1503
+ }
1504
+ function mapPt(x, y) {
1505
+ const px = x * scale + offsetX;
1506
+ const py = y * scale + offsetY;
1507
+ return [
1508
+ Math.max(0, Math.min(cols - 1, Math.round(px / W * (cols - 1)))),
1509
+ Math.max(0, Math.min(rows - 1, Math.round(py / H * (rows - 1))))
1510
+ ];
1511
+ }
1512
+ function stamp(c, r, intensity) {
1513
+ const idx = r * cols + c;
1514
+ if (intensity > grid[idx]) {
1515
+ grid[idx] = intensity;
1516
+ }
1517
+ }
1518
+ function buildGrid(deltaTime) {
1519
+ const trail = engine.tick(deltaTime);
1520
+ const count = engine.trailCount;
1521
+ grid.fill(0);
1522
+ for (let i = 0; i < count; i++) {
1523
+ const pt = trail[i];
1524
+ const intensity = (i + 1) / count;
1525
+ const [c, r] = mapPt(pt.x, pt.y);
1526
+ stamp(c, r, intensity);
1527
+ if (i < count - 1) {
1528
+ const next = trail[i + 1];
1529
+ const [nc, nr] = mapPt(next.x, next.y);
1530
+ const steps = Math.ceil(Math.max(Math.abs(nc - c), Math.abs(nr - r))) * 2;
1531
+ for (let s = 1; s < steps; s++) {
1532
+ const t = s / steps;
1533
+ const ix = pt.x + (next.x - pt.x) * t;
1534
+ const iy = pt.y + (next.y - pt.y) * t;
1535
+ const ii = intensity + 1 / count * t;
1536
+ const [ic, ir] = mapPt(ix, iy);
1537
+ stamp(ic, ir, ii);
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+ function draw() {
1543
+ ctx.clearRect(0, 0, W, H);
1544
+ if (bgCanvas) {
1545
+ ctx.drawImage(bgCanvas, 0, 0);
1546
+ }
1547
+ const animOffset = currentTrailStyle === "gradient-animated" ? Math.abs(animTime / ANIM_PERIOD % 2 - 1) * 0.35 : 0;
1548
+ for (let bucket = 0; bucket < NUM_BUCKETS; bucket++) {
1549
+ const lo = bucket / NUM_BUCKETS;
1550
+ const hi = (bucket + 1) / NUM_BUCKETS;
1551
+ const midpoint = (lo + hi) / 2;
1552
+ const alpha = 0.08 + midpoint * 0.92;
1553
+ let hasLit = false;
1554
+ ctx.beginPath();
1555
+ for (let row = 0; row < rows; row++) {
1556
+ for (let col = 0; col < cols; col++) {
1557
+ const intensity = grid[row * cols + col];
1558
+ if (intensity > lo && intensity <= hi) {
1559
+ const cx = (col + 0.5) * cellW;
1560
+ const cy = (row + 0.5) * cellH;
1561
+ ctx.roundRect(cx - dotR, cy - dotR, dotR * 2, dotR * 2, roundness * dotR);
1562
+ hasLit = true;
1563
+ }
1564
+ }
1565
+ }
1566
+ if (hasLit) {
1567
+ if (gradientRgb !== null) {
1568
+ const t = ((midpoint + animOffset) % 1 + 1) % 1;
1569
+ const { r, g, b } = sampleGradientRgb(gradientRgb, t);
1570
+ ctx.fillStyle = `rgb(${r},${g},${b})`;
1571
+ } else {
1572
+ const { r, g, b } = colorRgb;
1573
+ ctx.fillStyle = `rgb(${r},${g},${b})`;
1574
+ }
1575
+ ctx.globalAlpha = alpha;
1576
+ ctx.fill();
1577
+ }
1578
+ }
1579
+ ctx.globalAlpha = 1;
1580
+ }
1581
+ function renderFrame(deltaTime) {
1582
+ if (engine.morphAlpha !== null) {
1583
+ morphProgress = Math.min(1, morphProgress + deltaTime / (morphDurationMs / 1e3));
1584
+ engine.setMorphAlpha(morphProgress);
1585
+ calculateBoundaries(engine.getSarmalSkeleton());
1586
+ if (morphProgress >= 1) {
1587
+ engine.completeMorph();
1588
+ morphResolve?.();
1589
+ morphResolve = null;
1590
+ morphReject = null;
1591
+ morphProgress = 0;
1592
+ calculateBoundaries(engine.getSarmalSkeleton());
1593
+ }
1594
+ } else if (engine.isLiveSkeleton) {
1595
+ calculateBoundaries(engine.getSarmalSkeleton());
1596
+ }
1597
+ if (currentTrailStyle === "gradient-animated") {
1598
+ animTime += deltaTime;
1599
+ }
1600
+ buildGrid(deltaTime);
1601
+ draw();
1602
+ }
1603
+ function loop(timestamp = performance.now()) {
1604
+ const deltaTime = Math.min((timestamp - lastTime) / 1e3, 1 / 30);
1605
+ lastTime = timestamp;
1606
+ renderFrame(deltaTime);
1607
+ animationId = requestAnimationFrame(loop);
1608
+ }
1609
+ calculateBoundaries(engine.getSarmalSkeleton());
1610
+ buildBgCanvas();
1611
+ if (initialPhase !== void 0) {
1612
+ engine.seek(initialPhase);
1613
+ }
1614
+ renderFrame(0);
1615
+ const instance = {
1616
+ /** Starts the animation loop. Does nothing if already running. */
1617
+ play() {
1618
+ if (animationId !== null) return;
1619
+ lastTime = performance.now();
1620
+ loop();
1621
+ },
1622
+ /** Pauses the animation loop. Preserves current trail state. */
1623
+ pause() {
1624
+ if (animationId === null) return;
1625
+ cancelAnimationFrame(animationId);
1626
+ animationId = null;
1627
+ engine.cancelSpeedTransition();
1628
+ },
1629
+ /** Resets the animation to the start of the curve and clears the grid. */
1630
+ reset() {
1631
+ engine.reset();
1632
+ grid.fill(0);
1633
+ },
1634
+ /** Stops the animation and removes all event listeners. */
1635
+ destroy() {
1636
+ if (animationId !== null) {
1637
+ cancelAnimationFrame(animationId);
1638
+ animationId = null;
1639
+ }
1640
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1641
+ if (morphReject !== null) {
1642
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
1643
+ morphResolve = null;
1644
+ morphReject = null;
1645
+ }
1646
+ },
1647
+ ...enginePassthroughs(engine),
1648
+ /**
1649
+ * Smoothly transitions from the current curve to `target`.
1650
+ * If a morph is already in progress, it is snapped to completion before the new one starts.
1651
+ * @returns A Promise that resolves when the transition finishes.
1652
+ */
1653
+ morphTo(target, opts) {
1654
+ if (morphResolve !== null) {
1655
+ engine.completeMorph();
1656
+ morphResolve();
1657
+ morphResolve = null;
1658
+ morphReject = null;
1659
+ morphProgress = 0;
1660
+ }
1661
+ morphDurationMs = opts?.duration ?? DEFAULT_MORPH_DURATION_MS;
1662
+ morphProgress = 0;
1663
+ engine.startMorph(target, opts?.morphStrategy);
1664
+ return new Promise((resolve, reject) => {
1665
+ morphResolve = resolve;
1666
+ morphReject = reject;
1667
+ });
1668
+ },
1669
+ /**
1670
+ * Updates render options on a live instance without stopping the animation.
1671
+ *
1672
+ * Supported: `trailColor` and `trailStyle`.
1673
+ * ! Unsupported fields (`headColor`, `skeletonColor`, `headRadius`, `trailWidth`) throw.
1674
+ * ! Validation fails the entire call if any field is invalid, leaving options unchanged.
1675
+ */
1676
+ setRenderOptions(partial) {
1677
+ validateBaseRenderOptions(partial);
1678
+ let needsRebuildBg = false;
1679
+ if (partial.trailColor !== void 0) {
1680
+ if (Array.isArray(partial.trailColor)) {
1681
+ gradientRgb = partial.trailColor.map(colorToRgb);
1682
+ colorRgb = gradientRgb[0];
1683
+ } else {
1684
+ gradientRgb = null;
1685
+ colorRgb = colorToRgb(partial.trailColor);
1686
+ }
1687
+ needsRebuildBg = true;
1688
+ }
1689
+ if (partial.trailStyle !== void 0) {
1690
+ currentTrailStyle = partial.trailStyle;
1691
+ if (currentTrailStyle === "default") {
1692
+ animTime = 0;
1693
+ }
1694
+ }
1695
+ if (needsRebuildBg) {
1696
+ buildBgCanvas();
1697
+ }
1698
+ if (currentTrailStyle !== "default" && gradientRgb === null) {
1699
+ console.warn(
1700
+ "[sarmal] dot matrix: gradient trailStyle has no effect without a trailColor array"
1701
+ );
1702
+ }
1703
+ }
1704
+ };
1705
+ function handleVisibilityChange() {
1706
+ if (document.hidden) {
1707
+ if (animationId !== null) {
1708
+ instance.pause();
1709
+ pausedByVisibility = true;
1710
+ }
1711
+ } else {
1712
+ if (pausedByVisibility) {
1713
+ pausedByVisibility = false;
1714
+ instance.play();
1715
+ }
1716
+ }
1717
+ }
1718
+ if (pauseOnHiddenOpt) {
1719
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1720
+ }
1721
+ const shouldAutoStart = autoStart !== false;
1722
+ const actuallyAutoStart = shouldAutoStart && !(pauseOnHiddenOpt && document.hidden);
1723
+ if (actuallyAutoStart) {
1724
+ instance.play();
1725
+ } else if (shouldAutoStart) {
1726
+ pausedByVisibility = true;
1727
+ }
1728
+ return instance;
1729
+ }
1730
+
1364
1731
  // src/catmull-rom.ts
1365
1732
  var PERIOD = 2 * Math.PI;
1366
1733
  function catmullRom1D(p0, p1, p2, p3, u) {
@@ -1697,6 +2064,7 @@ exports.createEngine = createEngine;
1697
2064
  exports.createRenderer = createRenderer;
1698
2065
  exports.createSVGRenderer = createSVGRenderer;
1699
2066
  exports.createSarmal = createSarmal;
2067
+ exports.createSarmalDotMatrix = createSarmalDotMatrix;
1700
2068
  exports.createSarmalSVG = createSarmalSVG;
1701
2069
  exports.curves = curves;
1702
2070
  exports.deltoid = deltoid;