@sarmal/core 0.36.5 → 0.37.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 (46) hide show
  1. package/dist/auto-init.cjs +472 -2
  2. package/dist/auto-init.cjs.map +1 -1
  3. package/dist/auto-init.d.cts +6 -0
  4. package/dist/auto-init.d.ts +6 -0
  5. package/dist/auto-init.js +472 -2
  6. package/dist/auto-init.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.d.cts +3 -3
  38. package/dist/index.d.ts +3 -3
  39. package/dist/{renderer-shared-CFimm7VD.d.cts → renderer-shared-BDDSZhB6.d.cts} +1 -1
  40. package/dist/{renderer-shared-BZV9ELOa.d.ts → renderer-shared-D7ZDFZqK.d.ts} +1 -1
  41. package/dist/terminal.d.cts +2 -2
  42. package/dist/terminal.d.ts +2 -2
  43. package/dist/{types-CCgSK31t.d.cts → types-DS2VYCEa.d.cts} +3 -2
  44. package/dist/{types-CCgSK31t.d.ts → types-DS2VYCEa.d.ts} +3 -2
  45. package/package.json +2 -1
  46. package/skills/core/SKILL.md +180 -15
@@ -510,6 +510,11 @@ function getPaletteColor(palette, position, timeOffset = 0) {
510
510
  return lerpOklab(c1, c2, t);
511
511
  }
512
512
  var TRAIL_STYLES = ["default", "gradient-static", "gradient-animated"];
513
+ var BASE_RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
514
+ "trailColor",
515
+ "trailStyle",
516
+ "skeletonColor"
517
+ ]);
513
518
  var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
514
519
  "trailColor",
515
520
  "headColor",
@@ -518,6 +523,22 @@ var RENDER_OPTION_KEYS = /* @__PURE__ */ new Set([
518
523
  "headRadius",
519
524
  "trailWidth"
520
525
  ]);
526
+ function validateBaseRenderOptions(partial) {
527
+ for (const key of Object.keys(partial)) {
528
+ if (!BASE_RENDER_OPTION_KEYS.has(key)) {
529
+ throw new TypeError(`[sarmal] setRenderOptions: unsupported key "${key}" for this renderer`);
530
+ }
531
+ }
532
+ if (partial.trailColor !== void 0) {
533
+ assertTrailColor(partial.trailColor);
534
+ }
535
+ if (partial.trailStyle !== void 0) {
536
+ assertTrailStyle(partial.trailStyle);
537
+ }
538
+ if (partial.skeletonColor !== void 0) {
539
+ assertSkeletonColor(partial.skeletonColor);
540
+ }
541
+ }
521
542
  function validateRenderOptions(partial) {
522
543
  for (const key of Object.keys(partial)) {
523
544
  if (!RENDER_OPTION_KEYS.has(key)) {
@@ -1393,6 +1414,413 @@ function createSarmalSVG(container, curveDef, options) {
1393
1414
  return createSVGRenderer({ container, engine, ...rendererOpts });
1394
1415
  }
1395
1416
 
1417
+ // src/renderer-dot-matrix.ts
1418
+ function createSarmalDotMatrix(canvas, curveDef, options) {
1419
+ const {
1420
+ cols = 32,
1421
+ rows = 32,
1422
+ roundness = 1,
1423
+ trailLength: trailLengthOpt,
1424
+ trailColor: initialColor = "#ffffff",
1425
+ trailStyle: initialTrailStyle = "default",
1426
+ skeletonColor: skeletonColorOpt = "#ffffff",
1427
+ autoStart = true,
1428
+ pauseOnHidden: pauseOnHiddenOpt = true,
1429
+ initialPhase
1430
+ } = options ?? {};
1431
+ const trailLength = trailLengthOpt ?? cols * 3;
1432
+ const engine = createEngine(curveDef, trailLength);
1433
+ if (!canvas.getContext("2d")) {
1434
+ throw new Error("[sarmal] Could not get 2d context from canvas");
1435
+ }
1436
+ const ctx = canvas.getContext("2d");
1437
+ const W = canvas.width;
1438
+ const H = canvas.height;
1439
+ const cellW = W / cols;
1440
+ const cellH = H / rows;
1441
+ const dotR = Math.min(cellW, cellH) * 0.36;
1442
+ let gradientOklab = [];
1443
+ let colorRgb = { r: 255, g: 255, b: 255 };
1444
+ let trailStyle = initialTrailStyle;
1445
+ let trailColor = initialColor;
1446
+ let gradientAnimTime = 0;
1447
+ const grid = new Float32Array(cols * rows);
1448
+ let scale = 1;
1449
+ let offsetX = 0;
1450
+ let offsetY = 0;
1451
+ let pixelMaskStarts = new Uint32Array(0);
1452
+ let pixelMaskLengths = new Uint32Array(0);
1453
+ let pixelMaskIndices = new Uint32Array(0);
1454
+ let pixelMaskCoverages = new Float32Array(0);
1455
+ let bgImageData = null;
1456
+ let frameImageData = null;
1457
+ let skeletonColorOklab = null;
1458
+ const skeletonDotGrid = new Uint8Array(cols * rows);
1459
+ let animationId = null;
1460
+ let lastTime = 0;
1461
+ let pausedByVisibility = false;
1462
+ let morphResolve = null;
1463
+ let morphReject = null;
1464
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
1465
+ let morphProgress = 0;
1466
+ function computePixelMask() {
1467
+ const starts = new Uint32Array(cols * rows);
1468
+ const lengths = new Uint32Array(cols * rows);
1469
+ const allIndices = [];
1470
+ const allCoverages = [];
1471
+ const cornerR = roundness * dotR;
1472
+ const cornerR2 = cornerR * cornerR;
1473
+ const SSAA = 4;
1474
+ const SSAA2 = SSAA * SSAA;
1475
+ for (let row = 0; row < rows; row++) {
1476
+ for (let col = 0; col < cols; col++) {
1477
+ const dotIdx = row * cols + col;
1478
+ const cx = (col + 0.5) * cellW;
1479
+ const cy = (row + 0.5) * cellH;
1480
+ const x0 = Math.max(0, Math.floor(cx - dotR - 1));
1481
+ const x1 = Math.min(W - 1, Math.ceil(cx + dotR + 1));
1482
+ const y0 = Math.max(0, Math.floor(cy - dotR - 1));
1483
+ const y1 = Math.min(H - 1, Math.ceil(cy + dotR + 1));
1484
+ starts[dotIdx] = allIndices.length;
1485
+ let count = 0;
1486
+ for (let py = y0; py <= y1; py++) {
1487
+ for (let px = x0; px <= x1; px++) {
1488
+ let hits = 0;
1489
+ for (let sy = 0; sy < SSAA; sy++) {
1490
+ const spyCenter = py + (sy + 0.5) / SSAA;
1491
+ for (let sx = 0; sx < SSAA; sx++) {
1492
+ const spxCenter = px + (sx + 0.5) / SSAA;
1493
+ const dx = Math.max(Math.abs(spxCenter - cx) - (dotR - cornerR), 0);
1494
+ const dy = Math.max(Math.abs(spyCenter - cy) - (dotR - cornerR), 0);
1495
+ if (dx * dx + dy * dy <= cornerR2) {
1496
+ hits++;
1497
+ }
1498
+ }
1499
+ }
1500
+ if (hits > 0) {
1501
+ allIndices.push((py * W + px) * 4);
1502
+ allCoverages.push(hits / SSAA2);
1503
+ count++;
1504
+ }
1505
+ }
1506
+ }
1507
+ lengths[dotIdx] = count;
1508
+ }
1509
+ }
1510
+ pixelMaskStarts = starts;
1511
+ pixelMaskLengths = lengths;
1512
+ pixelMaskIndices = new Uint32Array(allIndices);
1513
+ pixelMaskCoverages = new Float32Array(allCoverages);
1514
+ }
1515
+ function buildBgImageData() {
1516
+ bgImageData = new ImageData(W, H);
1517
+ const bg = colorRgb;
1518
+ const baseAlpha = 0.05 * 255;
1519
+ const { data } = bgImageData;
1520
+ const n = cols * rows;
1521
+ for (let dotIdx = 0; dotIdx < n; dotIdx++) {
1522
+ const start = pixelMaskStarts[dotIdx];
1523
+ const len = pixelMaskLengths[dotIdx];
1524
+ for (let k = 0; k < len; k++) {
1525
+ const px = pixelMaskIndices[start + k];
1526
+ const coverage = pixelMaskCoverages[start + k];
1527
+ data[px] = bg.r;
1528
+ data[px + 1] = bg.g;
1529
+ data[px + 2] = bg.b;
1530
+ data[px + 3] = Math.round(baseAlpha * coverage);
1531
+ }
1532
+ }
1533
+ }
1534
+ function applyColor(color) {
1535
+ colorRgb = colorToRgb(resolveTrailMainColor(color));
1536
+ gradientOklab = resolveTrailPalette(color).map((c) => parseColorToOklab(c));
1537
+ }
1538
+ function applySkeletonColor(color) {
1539
+ skeletonColorOklab = color === "transparent" ? null : parseColorToOklab(color);
1540
+ }
1541
+ function computeSkeletonGrid(skel) {
1542
+ skeletonDotGrid.fill(0);
1543
+ const count = skel.length;
1544
+ for (let i = 0; i < count; i++) {
1545
+ const pt = skel[i];
1546
+ const [c, r] = mapPt(pt.x, pt.y);
1547
+ skeletonDotGrid[r * cols + c] = 1;
1548
+ if (i < count - 1) {
1549
+ const next = skel[i + 1];
1550
+ const [nc, nr] = mapPt(next.x, next.y);
1551
+ const steps = Math.ceil(Math.max(Math.abs(nc - c), Math.abs(nr - r))) * 2;
1552
+ for (let s = 1; s < steps; s++) {
1553
+ const t = s / steps;
1554
+ const ix = pt.x + (next.x - pt.x) * t;
1555
+ const iy = pt.y + (next.y - pt.y) * t;
1556
+ const [ic, ir] = mapPt(ix, iy);
1557
+ skeletonDotGrid[ir * cols + ic] = 1;
1558
+ }
1559
+ }
1560
+ }
1561
+ }
1562
+ function writeSkeletonPixels(data) {
1563
+ if (skeletonColorOklab === null) {
1564
+ return;
1565
+ }
1566
+ const { r, g, b } = oklabToRgb(skeletonColorOklab);
1567
+ const skelBaseAlpha = DEFAULT_SKELETON_OPACITY * 255;
1568
+ const n = cols * rows;
1569
+ for (let dotIdx = 0; dotIdx < n; dotIdx++) {
1570
+ if (!skeletonDotGrid[dotIdx]) {
1571
+ continue;
1572
+ }
1573
+ const start = pixelMaskStarts[dotIdx];
1574
+ const len = pixelMaskLengths[dotIdx];
1575
+ for (let k = 0; k < len; k++) {
1576
+ const px = pixelMaskIndices[start + k];
1577
+ const coverage = pixelMaskCoverages[start + k];
1578
+ data[px] = r;
1579
+ data[px + 1] = g;
1580
+ data[px + 2] = b;
1581
+ data[px + 3] = Math.round(skelBaseAlpha * coverage);
1582
+ }
1583
+ }
1584
+ }
1585
+ function calculateBoundaries(skel) {
1586
+ const b = computeBoundaries(skel, W, H);
1587
+ if (b) {
1588
+ scale = b.scale;
1589
+ offsetX = b.offsetX;
1590
+ offsetY = b.offsetY;
1591
+ }
1592
+ }
1593
+ function mapPt(x, y) {
1594
+ const px = x * scale + offsetX;
1595
+ const py = y * scale + offsetY;
1596
+ return [
1597
+ Math.max(0, Math.min(cols - 1, Math.round(px / W * (cols - 1)))),
1598
+ Math.max(0, Math.min(rows - 1, Math.round(py / H * (rows - 1))))
1599
+ ];
1600
+ }
1601
+ function stamp(c, r, intensity) {
1602
+ const idx = r * cols + c;
1603
+ if (intensity > grid[idx]) {
1604
+ grid[idx] = intensity;
1605
+ }
1606
+ }
1607
+ function buildGrid(deltaTime) {
1608
+ const trail = engine.tick(deltaTime);
1609
+ const count = engine.trailCount;
1610
+ grid.fill(0);
1611
+ for (let i = 0; i < count; i++) {
1612
+ const pt = trail[i];
1613
+ const intensity = (i + 1) / count;
1614
+ const [c, r] = mapPt(pt.x, pt.y);
1615
+ stamp(c, r, intensity);
1616
+ if (i < count - 1) {
1617
+ const next = trail[i + 1];
1618
+ const [nc, nr] = mapPt(next.x, next.y);
1619
+ const steps = Math.ceil(Math.max(Math.abs(nc - c), Math.abs(nr - r))) * 2;
1620
+ for (let s = 1; s < steps; s++) {
1621
+ const t = s / steps;
1622
+ const ix = pt.x + (next.x - pt.x) * t;
1623
+ const iy = pt.y + (next.y - pt.y) * t;
1624
+ const ii = intensity + 1 / count * t;
1625
+ const [ic, ir] = mapPt(ix, iy);
1626
+ stamp(ic, ir, ii);
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ function draw() {
1632
+ if (!bgImageData || !frameImageData) {
1633
+ return;
1634
+ }
1635
+ frameImageData.data.set(bgImageData.data);
1636
+ const { data } = frameImageData;
1637
+ writeSkeletonPixels(data);
1638
+ const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
1639
+ const n = cols * rows;
1640
+ for (let dotIdx = 0; dotIdx < n; dotIdx++) {
1641
+ const intensity = grid[dotIdx];
1642
+ if (intensity <= 0) {
1643
+ continue;
1644
+ }
1645
+ let r, g, b;
1646
+ if (trailStyle !== "default") {
1647
+ ({ r, g, b } = oklabToRgb(getPaletteColor(gradientOklab, intensity, timeOffset)));
1648
+ } else {
1649
+ ({ r, g, b } = colorRgb);
1650
+ }
1651
+ const skelFloor = skeletonColorOklab !== null && skeletonDotGrid[dotIdx] === 1 ? DEFAULT_SKELETON_OPACITY * 255 : 0;
1652
+ const baseA = Math.max(skelFloor, (0.08 + intensity * 0.92) * 255);
1653
+ const start = pixelMaskStarts[dotIdx];
1654
+ const len = pixelMaskLengths[dotIdx];
1655
+ for (let k = 0; k < len; k++) {
1656
+ const px = pixelMaskIndices[start + k];
1657
+ const coverage = pixelMaskCoverages[start + k];
1658
+ data[px] = r;
1659
+ data[px + 1] = g;
1660
+ data[px + 2] = b;
1661
+ data[px + 3] = Math.round(baseA * coverage);
1662
+ }
1663
+ }
1664
+ ctx.putImageData(frameImageData, 0, 0);
1665
+ }
1666
+ function completeMorphNow() {
1667
+ engine.completeMorph();
1668
+ morphResolve?.();
1669
+ morphResolve = null;
1670
+ morphReject = null;
1671
+ morphProgress = 0;
1672
+ }
1673
+ function renderFrame(deltaTime) {
1674
+ if (engine.morphAlpha !== null) {
1675
+ morphProgress = Math.min(1, morphProgress + deltaTime / (morphDurationMs / 1e3));
1676
+ engine.setMorphAlpha(morphProgress);
1677
+ calculateBoundaries(engine.getSarmalSkeleton());
1678
+ if (morphProgress >= 1) {
1679
+ completeMorphNow();
1680
+ calculateBoundaries(engine.getSarmalSkeleton());
1681
+ }
1682
+ computeSkeletonGrid(engine.getSarmalSkeleton());
1683
+ } else if (engine.isLiveSkeleton) {
1684
+ calculateBoundaries(engine.getSarmalSkeleton());
1685
+ computeSkeletonGrid(engine.getSarmalSkeleton());
1686
+ }
1687
+ if (trailStyle === "gradient-animated") {
1688
+ gradientAnimTime += deltaTime * 1e3;
1689
+ }
1690
+ buildGrid(deltaTime);
1691
+ draw();
1692
+ }
1693
+ function loop(timestamp = performance.now()) {
1694
+ const deltaTime = Math.min((timestamp - lastTime) / 1e3, 1 / 30);
1695
+ lastTime = timestamp;
1696
+ renderFrame(deltaTime);
1697
+ animationId = requestAnimationFrame(loop);
1698
+ }
1699
+ validateBaseRenderOptions({ trailColor: initialColor, skeletonColor: skeletonColorOpt });
1700
+ applyColor(initialColor);
1701
+ applySkeletonColor(skeletonColorOpt);
1702
+ warnIfTrailColorMismatch(trailColor, trailStyle);
1703
+ calculateBoundaries(engine.getSarmalSkeleton());
1704
+ computePixelMask();
1705
+ computeSkeletonGrid(engine.getSarmalSkeleton());
1706
+ frameImageData = new ImageData(W, H);
1707
+ buildBgImageData();
1708
+ if (initialPhase !== void 0) {
1709
+ engine.seek(initialPhase);
1710
+ }
1711
+ renderFrame(0);
1712
+ const instance = {
1713
+ /** Starts the animation loop. Does nothing if already running. */
1714
+ play() {
1715
+ if (animationId !== null) {
1716
+ return;
1717
+ }
1718
+ lastTime = performance.now();
1719
+ loop();
1720
+ },
1721
+ /** Pauses the animation loop. Preserves current trail state. */
1722
+ pause() {
1723
+ if (animationId === null) {
1724
+ return;
1725
+ }
1726
+ cancelAnimationFrame(animationId);
1727
+ animationId = null;
1728
+ engine.cancelSpeedTransition();
1729
+ },
1730
+ /** Resets the animation to the start of the curve and clears the grid. */
1731
+ reset() {
1732
+ engine.reset();
1733
+ grid.fill(0);
1734
+ },
1735
+ /** Stops the animation and removes all event listeners. */
1736
+ destroy() {
1737
+ if (animationId !== null) {
1738
+ cancelAnimationFrame(animationId);
1739
+ animationId = null;
1740
+ }
1741
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1742
+ if (morphReject !== null) {
1743
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
1744
+ morphResolve = null;
1745
+ morphReject = null;
1746
+ }
1747
+ },
1748
+ ...enginePassthroughs(engine),
1749
+ /**
1750
+ * Smoothly transitions from the current curve to `target`.
1751
+ * If a morph is already in progress, it is snapped to completion before the new one starts.
1752
+ * @returns A Promise that resolves when the transition finishes.
1753
+ */
1754
+ morphTo(target, opts) {
1755
+ if (morphResolve !== null) {
1756
+ completeMorphNow();
1757
+ }
1758
+ morphDurationMs = opts?.duration ?? DEFAULT_MORPH_DURATION_MS;
1759
+ morphProgress = 0;
1760
+ engine.startMorph(target, opts?.morphStrategy);
1761
+ return new Promise((resolve, reject) => {
1762
+ morphResolve = resolve;
1763
+ morphReject = reject;
1764
+ });
1765
+ },
1766
+ /**
1767
+ * Updates render options on a live instance without stopping the animation.
1768
+ *
1769
+ * Supported: `trailColor`, `trailStyle`, and `skeletonColor`.
1770
+ * ! Unsupported fields (`headColor`, `headRadius`, `trailWidth`) throw.
1771
+ * ! Validation fails the entire call if any field is invalid, leaving options unchanged.
1772
+ */
1773
+ setRenderOptions(partial) {
1774
+ validateBaseRenderOptions(partial);
1775
+ let needsRebuildBg = false;
1776
+ if (partial.trailColor !== void 0) {
1777
+ trailColor = partial.trailColor;
1778
+ applyColor(trailColor);
1779
+ needsRebuildBg = true;
1780
+ }
1781
+ if (partial.skeletonColor !== void 0) {
1782
+ applySkeletonColor(partial.skeletonColor);
1783
+ }
1784
+ if (partial.trailStyle !== void 0) {
1785
+ trailStyle = partial.trailStyle;
1786
+ if (trailStyle === "default") {
1787
+ gradientAnimTime = 0;
1788
+ }
1789
+ }
1790
+ if (needsRebuildBg) {
1791
+ buildBgImageData();
1792
+ }
1793
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1794
+ warnIfTrailColorMismatch(trailColor, trailStyle);
1795
+ }
1796
+ }
1797
+ };
1798
+ function handleVisibilityChange() {
1799
+ if (document.hidden) {
1800
+ if (animationId !== null) {
1801
+ instance.pause();
1802
+ pausedByVisibility = true;
1803
+ }
1804
+ } else {
1805
+ if (pausedByVisibility) {
1806
+ pausedByVisibility = false;
1807
+ instance.play();
1808
+ }
1809
+ }
1810
+ }
1811
+ if (pauseOnHiddenOpt) {
1812
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1813
+ }
1814
+ const shouldAutoStart = autoStart !== false;
1815
+ const actuallyAutoStart = shouldAutoStart && !(pauseOnHiddenOpt && document.hidden);
1816
+ if (actuallyAutoStart) {
1817
+ instance.play();
1818
+ } else if (shouldAutoStart) {
1819
+ pausedByVisibility = true;
1820
+ }
1821
+ return instance;
1822
+ }
1823
+
1396
1824
  // src/catmull-rom.ts
1397
1825
  var PERIOD = 2 * Math.PI;
1398
1826
  function catmullRom1D(p0, p1, p2, p3, u) {
@@ -1733,7 +2161,15 @@ function parseTrailColor(value) {
1733
2161
  }
1734
2162
  return value;
1735
2163
  }
2164
+ function parseBoolAttr(value) {
2165
+ if (value === void 0) {
2166
+ return void 0;
2167
+ }
2168
+ return value !== "false";
2169
+ }
1736
2170
  function buildOptions(el2) {
2171
+ const autoStart = parseBoolAttr(el2.dataset.autoStart);
2172
+ const pauseOnHidden = parseBoolAttr(el2.dataset.pauseOnHidden);
1737
2173
  return {
1738
2174
  ...el2.dataset.trailColor && {
1739
2175
  trailColor: parseTrailColor(el2.dataset.trailColor)
@@ -1744,9 +2180,38 @@ function buildOptions(el2) {
1744
2180
  ...el2.dataset.trailLength && { trailLength: parseInt(el2.dataset.trailLength, 10) },
1745
2181
  ...el2.dataset.trailStyle && {
1746
2182
  trailStyle: el2.dataset.trailStyle
2183
+ },
2184
+ ...el2.dataset.trailWidth && { trailWidth: parseFloat(el2.dataset.trailWidth) },
2185
+ ...autoStart !== void 0 && { autoStart },
2186
+ ...pauseOnHidden !== void 0 && { pauseOnHidden },
2187
+ // `!==` undefined so that data-initial-phase="0" is correctly passed as 0
2188
+ ...el2.dataset.initialPhase !== void 0 && {
2189
+ initialPhase: parseFloat(el2.dataset.initialPhase)
1747
2190
  }
1748
2191
  };
1749
2192
  }
2193
+ function buildDotMatrixOptions(el2) {
2194
+ const autoStart = parseBoolAttr(el2.dataset.autoStart);
2195
+ const pauseOnHidden = parseBoolAttr(el2.dataset.pauseOnHidden);
2196
+ return {
2197
+ ...el2.dataset.trailColor && {
2198
+ trailColor: parseTrailColor(el2.dataset.trailColor)
2199
+ },
2200
+ ...el2.dataset.skeletonColor && { skeletonColor: el2.dataset.skeletonColor },
2201
+ ...el2.dataset.trailStyle && {
2202
+ trailStyle: el2.dataset.trailStyle
2203
+ },
2204
+ ...el2.dataset.trailLength && { trailLength: parseInt(el2.dataset.trailLength, 10) },
2205
+ ...autoStart !== void 0 && { autoStart },
2206
+ ...pauseOnHidden !== void 0 && { pauseOnHidden },
2207
+ ...el2.dataset.initialPhase !== void 0 && {
2208
+ initialPhase: parseFloat(el2.dataset.initialPhase)
2209
+ },
2210
+ ...el2.dataset.cols && { cols: parseInt(el2.dataset.cols, 10) },
2211
+ ...el2.dataset.rows && { rows: parseInt(el2.dataset.rows, 10) },
2212
+ ...el2.dataset.roundness && { roundness: parseFloat(el2.dataset.roundness) }
2213
+ };
2214
+ }
1750
2215
  function init() {
1751
2216
  const elements = document.querySelectorAll("canvas[data-sarmal], svg[data-sarmal]");
1752
2217
  elements.forEach((el2) => {
@@ -1758,8 +2223,13 @@ function init() {
1758
2223
  if (!curveDef) {
1759
2224
  return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
1760
2225
  }
1761
- const options = buildOptions(el2);
1762
- const instance = el2 instanceof HTMLCanvasElement ? createSarmal(el2, curveDef, options) : createSarmalSVG(el2, curveDef, options);
2226
+ const renderer = el2.dataset.renderer;
2227
+ if (el2 instanceof HTMLCanvasElement && renderer !== void 0 && renderer !== "canvas" && renderer !== "dot-matrix") {
2228
+ return console.error(
2229
+ `[sarmal] Unknown data-renderer value: "${renderer}". Expected "canvas" or "dot-matrix".`
2230
+ );
2231
+ }
2232
+ const instance = el2 instanceof HTMLCanvasElement && renderer === "dot-matrix" ? createSarmalDotMatrix(el2, curveDef, buildDotMatrixOptions(el2)) : el2 instanceof HTMLCanvasElement ? createSarmal(el2, curveDef, buildOptions(el2)) : createSarmalSVG(el2, curveDef, buildOptions(el2));
1763
2233
  if (el2.dataset.speed) {
1764
2234
  instance.setSpeed(parseFloat(el2.dataset.speed));
1765
2235
  }