@sarmal/core 0.37.1 → 0.38.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 (52) hide show
  1. package/README.md +4 -4
  2. package/dist/auto-init.cjs +111 -14
  3. package/dist/auto-init.cjs.map +1 -1
  4. package/dist/auto-init.js +111 -14
  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 +111 -14
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +12 -53
  40. package/dist/index.d.ts +12 -53
  41. package/dist/index.js +111 -14
  42. package/dist/index.js.map +1 -1
  43. package/dist/{renderer-shared-DWPVHjKZ.d.ts → renderer-shared-2tEwOWJm.d.ts} +1 -1
  44. package/dist/{renderer-shared-Bcc_1IaT.d.cts → renderer-shared-Bh33A5Av.d.cts} +1 -1
  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-B1XeFpuq.d.cts → types-CmPFR9U3.d.cts} +126 -2
  50. package/dist/{types-B1XeFpuq.d.ts → types-CmPFR9U3.d.ts} +126 -2
  51. package/package.json +5 -5
  52. package/skills/core/SKILL.md +3 -3
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @sarmal/core
2
2
 
3
3
  <p align="center">
4
- <strong>Parametric curve animations for loading/thinking indicators</strong>
4
+ <strong>Beautiful animated curves for loading states and ambient motion</strong>
5
5
  </p>
6
6
 
7
7
  <div align="center">
@@ -12,14 +12,14 @@
12
12
 
13
13
  ---
14
14
 
15
- **@sarmal/core** is a lightweight library for rendering elegant parametric curve animations.
15
+ **@sarmal/core** is a lightweight library for animated curves on canvas and SVG.
16
16
 
17
- The animations can be used anywhere you want. Use it for loading spinners, progress indicators, or to indicate that your very special AI model is _thinking_, up to you.
17
+ Use it for loading animations, thinking indicators, ambient decoration or whatever else you have in mind.
18
18
 
19
19
  In web applications or directly in your terminal (`npx @sarmal/core`).
20
20
 
21
21
  - **Canvas & SVG renderers**: choose one or the other, but why not both?
22
- - **standard curves**: default cliche curves any LLM can generate in seconds, from classic spirals to custom parametric paths
22
+ - **standard curves**: classic spirals, roses, Lissajous figures plus draw your own with point arrays
23
23
  - **TIME CONTROL**: programmatic time stepping, seeking, and trail effects
24
24
  - **Zero dependencies**: tiny bundle, quick to get started
25
25
  - **TypeScript-first**: because who would build anyhing complex in pure JS?!
@@ -364,15 +364,42 @@ function computeBoundaries(pts, logicalWidth, logicalHeight, minPaddingPx = FIT_
364
364
  offsetY: (logicalHeight - h * scale) / 2 - minY * scale
365
365
  };
366
366
  }
367
- function enginePassthroughs(engine) {
367
+ var DESTROYED_ERROR = "[sarmal] Instance has been destroyed and cannot be used again. Call pause() instead of destroy() for temporary suspension.";
368
+ function enginePassthroughs(engine, isDestroyed) {
369
+ function guard() {
370
+ if (isDestroyed()) {
371
+ throw new Error(DESTROYED_ERROR);
372
+ }
373
+ }
368
374
  return {
369
- jump: engine.jump,
370
- seek: engine.seek,
371
- setSpeed: engine.setSpeed,
372
- getSpeed: engine.getSpeed,
373
- resetSpeed: engine.resetSpeed,
374
- setSpeedOver: engine.setSpeedOver,
375
- getSarmalSkeleton: engine.getSarmalSkeleton
375
+ jump(phase, options) {
376
+ guard();
377
+ engine.jump(phase, options);
378
+ },
379
+ seek(phase, options) {
380
+ guard();
381
+ engine.seek(phase, options);
382
+ },
383
+ setSpeed(speed) {
384
+ guard();
385
+ engine.setSpeed(speed);
386
+ },
387
+ getSpeed() {
388
+ guard();
389
+ return engine.getSpeed();
390
+ },
391
+ resetSpeed() {
392
+ guard();
393
+ engine.resetSpeed();
394
+ },
395
+ setSpeedOver(speed, duration) {
396
+ guard();
397
+ return engine.setSpeedOver(speed, duration);
398
+ },
399
+ getSarmalSkeleton() {
400
+ guard();
401
+ return engine.getSarmalSkeleton();
402
+ }
376
403
  };
377
404
  }
378
405
  function hexToRgb(hex) {
@@ -731,6 +758,7 @@ function createRenderer(options) {
731
758
  let animationId = null;
732
759
  let lastTime = 0;
733
760
  let pausedByVisibility = false;
761
+ let destroyed = false;
734
762
  let morphResolve = null;
735
763
  let morphReject = null;
736
764
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
@@ -902,6 +930,9 @@ function createRenderer(options) {
902
930
  const shouldAutoStart = options.autoStart !== false;
903
931
  const instance = {
904
932
  play() {
933
+ if (destroyed) {
934
+ throw new Error(DESTROYED_ERROR);
935
+ }
905
936
  if (animationId !== null) {
906
937
  return;
907
938
  }
@@ -909,6 +940,9 @@ function createRenderer(options) {
909
940
  loop();
910
941
  },
911
942
  pause() {
943
+ if (destroyed) {
944
+ throw new Error(DESTROYED_ERROR);
945
+ }
912
946
  if (animationId === null) {
913
947
  return;
914
948
  }
@@ -917,24 +951,36 @@ function createRenderer(options) {
917
951
  engine.cancelSpeedTransition();
918
952
  },
919
953
  reset() {
954
+ if (destroyed) {
955
+ throw new Error(DESTROYED_ERROR);
956
+ }
920
957
  engine.reset();
921
958
  trail = [];
922
959
  head = null;
923
960
  },
924
961
  destroy() {
962
+ if (destroyed) {
963
+ return;
964
+ }
965
+ destroyed = true;
925
966
  if (animationId !== null) {
926
967
  cancelAnimationFrame(animationId);
927
968
  animationId = null;
928
969
  }
929
970
  document.removeEventListener("visibilitychange", handleVisibilityChange);
971
+ engine.cancelSpeedTransition();
930
972
  if (morphReject !== null) {
931
- morphReject(new Error("Instance destroyed during morph"));
973
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
932
974
  morphResolve = null;
933
975
  morphReject = null;
934
976
  }
977
+ ctx.clearRect(0, 0, logicalWidth, logicalHeight);
935
978
  },
936
- ...enginePassthroughs(engine),
979
+ ...enginePassthroughs(engine, () => destroyed),
937
980
  morphTo(target, options2) {
981
+ if (destroyed) {
982
+ throw new Error(DESTROYED_ERROR);
983
+ }
938
984
  if (morphResolve !== null) {
939
985
  engine.completeMorph();
940
986
  morphResolve();
@@ -951,6 +997,9 @@ function createRenderer(options) {
951
997
  });
952
998
  },
953
999
  setRenderOptions(partial) {
1000
+ if (destroyed) {
1001
+ throw new Error(DESTROYED_ERROR);
1002
+ }
954
1003
  validateRenderOptions(partial);
955
1004
  if (partial.trailColor !== void 0) {
956
1005
  trailColor = partial.trailColor;
@@ -1205,6 +1254,7 @@ function createSVGRenderer(options) {
1205
1254
  let animationId = null;
1206
1255
  let lastTime = 0;
1207
1256
  let pausedByVisibility = false;
1257
+ let destroyed = false;
1208
1258
  const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1209
1259
  let morphResolve = null;
1210
1260
  let morphReject = null;
@@ -1273,6 +1323,9 @@ function createSVGRenderer(options) {
1273
1323
  const shouldAutoStart = options.autoStart !== false;
1274
1324
  const instance = {
1275
1325
  play() {
1326
+ if (destroyed) {
1327
+ throw new Error(DESTROYED_ERROR);
1328
+ }
1276
1329
  if (animationId !== null) {
1277
1330
  return;
1278
1331
  }
@@ -1280,6 +1333,9 @@ function createSVGRenderer(options) {
1280
1333
  loop();
1281
1334
  },
1282
1335
  pause() {
1336
+ if (destroyed) {
1337
+ throw new Error(DESTROYED_ERROR);
1338
+ }
1283
1339
  if (animationId === null) {
1284
1340
  return;
1285
1341
  }
@@ -1288,23 +1344,34 @@ function createSVGRenderer(options) {
1288
1344
  engine.cancelSpeedTransition();
1289
1345
  },
1290
1346
  reset() {
1347
+ if (destroyed) {
1348
+ throw new Error(DESTROYED_ERROR);
1349
+ }
1291
1350
  engine.reset();
1292
1351
  },
1293
1352
  destroy() {
1353
+ if (destroyed) {
1354
+ return;
1355
+ }
1356
+ destroyed = true;
1294
1357
  if (animationId !== null) {
1295
1358
  cancelAnimationFrame(animationId);
1296
1359
  animationId = null;
1297
1360
  }
1298
1361
  document.removeEventListener("visibilitychange", handleVisibilityChange);
1362
+ engine.cancelSpeedTransition();
1299
1363
  if (morphReject !== null) {
1300
- morphReject(new Error("Instance destroyed during morph"));
1364
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
1301
1365
  morphResolve = null;
1302
1366
  morphReject = null;
1303
1367
  }
1304
1368
  group.remove();
1305
1369
  },
1306
- ...enginePassthroughs(engine),
1370
+ ...enginePassthroughs(engine, () => destroyed),
1307
1371
  morphTo(target, options2) {
1372
+ if (destroyed) {
1373
+ throw new Error(DESTROYED_ERROR);
1374
+ }
1308
1375
  if (morphResolve !== null) {
1309
1376
  engine.completeMorph();
1310
1377
  morphResolve();
@@ -1330,6 +1397,9 @@ function createSVGRenderer(options) {
1330
1397
  });
1331
1398
  },
1332
1399
  setRenderOptions(partial) {
1400
+ if (destroyed) {
1401
+ throw new Error(DESTROYED_ERROR);
1402
+ }
1333
1403
  validateRenderOptions(partial);
1334
1404
  const prevTrailStyle = trailStyle;
1335
1405
  if (partial.trailColor !== void 0) {
@@ -1459,6 +1529,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1459
1529
  let animationId = null;
1460
1530
  let lastTime = 0;
1461
1531
  let pausedByVisibility = false;
1532
+ let destroyed = false;
1462
1533
  let morphResolve = null;
1463
1534
  let morphReject = null;
1464
1535
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
@@ -1712,6 +1783,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1712
1783
  const instance = {
1713
1784
  /** Starts the animation loop. Does nothing if already running. */
1714
1785
  play() {
1786
+ if (destroyed) {
1787
+ throw new Error(DESTROYED_ERROR);
1788
+ }
1715
1789
  if (animationId !== null) {
1716
1790
  return;
1717
1791
  }
@@ -1720,6 +1794,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1720
1794
  },
1721
1795
  /** Pauses the animation loop. Preserves current trail state. */
1722
1796
  pause() {
1797
+ if (destroyed) {
1798
+ throw new Error(DESTROYED_ERROR);
1799
+ }
1723
1800
  if (animationId === null) {
1724
1801
  return;
1725
1802
  }
@@ -1729,29 +1806,46 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1729
1806
  },
1730
1807
  /** Resets the animation to the start of the curve and clears the grid. */
1731
1808
  reset() {
1809
+ if (destroyed) {
1810
+ throw new Error(DESTROYED_ERROR);
1811
+ }
1732
1812
  engine.reset();
1733
1813
  grid.fill(0);
1734
1814
  },
1735
- /** Stops the animation and removes all event listeners. */
1815
+ /**
1816
+ * Permanently stops the animation and clears the visual output.
1817
+ * Calling any method on a destroyed instance throws an error.
1818
+ * `destroy()` is idempotent — calling it multiple times is safe.
1819
+ */
1736
1820
  destroy() {
1821
+ if (destroyed) {
1822
+ return;
1823
+ }
1824
+ destroyed = true;
1737
1825
  if (animationId !== null) {
1738
1826
  cancelAnimationFrame(animationId);
1739
1827
  animationId = null;
1740
1828
  }
1741
1829
  document.removeEventListener("visibilitychange", handleVisibilityChange);
1830
+ engine.cancelSpeedTransition();
1742
1831
  if (morphReject !== null) {
1743
1832
  morphReject(new Error("[sarmal] Instance destroyed during morph"));
1744
1833
  morphResolve = null;
1745
1834
  morphReject = null;
1746
1835
  }
1836
+ grid.fill(0);
1837
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1747
1838
  },
1748
- ...enginePassthroughs(engine),
1839
+ ...enginePassthroughs(engine, () => destroyed),
1749
1840
  /**
1750
1841
  * Smoothly transitions from the current curve to `target`.
1751
1842
  * If a morph is already in progress, it is snapped to completion before the new one starts.
1752
1843
  * @returns A Promise that resolves when the transition finishes.
1753
1844
  */
1754
1845
  morphTo(target, opts) {
1846
+ if (destroyed) {
1847
+ throw new Error(DESTROYED_ERROR);
1848
+ }
1755
1849
  if (morphResolve !== null) {
1756
1850
  completeMorphNow();
1757
1851
  }
@@ -1771,6 +1865,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1771
1865
  * ! Validation fails the entire call if any field is invalid, leaving options unchanged.
1772
1866
  */
1773
1867
  setRenderOptions(partial) {
1868
+ if (destroyed) {
1869
+ throw new Error(DESTROYED_ERROR);
1870
+ }
1774
1871
  validateBaseRenderOptions(partial);
1775
1872
  let needsRebuildBg = false;
1776
1873
  if (partial.trailColor !== void 0) {