@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/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { B as BaseRendererOptions, E as Engine, S as SarmalInstance, C as CurveDef, T as TrailColor, a as TrailStyle, D as DotMatrixRuntimeRenderOptions, R as RendererOptions, b as ControlPoint, P as Point, c as SarmalOptions } from './types-B1XeFpuq.cjs';
2
- export { d as BaseRuntimeRenderOptions, J as JumpOptions, M as MorphStrategy, e as RuntimeRenderOptions, f as SeekOptions } from './types-B1XeFpuq.cjs';
1
+ import { B as BaseRendererOptions, E as Engine, S as SarmalInstance, C as CurveDef, R as RendererOptions, D as DotMatrixSarmalOptions, a as DotMatrixRuntimeRenderOptions, b as ControlPoint, P as Point, c as SarmalOptions } from './types-CmPFR9U3.cjs';
2
+ export { d as BaseDotMatrixOptions, e as BaseInit, f as BaseRuntimeRenderOptions, g as BaseSarmalOptions, h as CanvasInit, i as DotMatrixInit, J as JumpOptions, M as MorphStrategy, j as RuntimeRenderOptions, k as SeekOptions, T as TrailColor, l as TrailStyle } from './types-CmPFR9U3.cjs';
3
3
  export { CurveName, curves } from './curves/index.cjs';
4
- export { B as BoundaryResult, S as SarmalPalette, c as computeBoundaries, p as palettes } from './renderer-shared-Bcc_1IaT.cjs';
4
+ export { B as BoundaryResult, S as SarmalPalette, c as computeBoundaries, p as palettes } from './renderer-shared-Bh33A5Av.cjs';
5
5
  export { artemis2 } from './curves/artemis2.cjs';
6
6
  export { astroid } from './curves/astroid.cjs';
7
7
  export { deltoid } from './curves/deltoid.cjs';
@@ -53,47 +53,14 @@ declare function createSVGRenderer(options: SVGRendererOptions): SarmalInstance;
53
53
  */
54
54
  declare function createSarmalSVG(container: SVGSVGElement, curveDef: CurveDef, options?: SVGSarmalOptions): SarmalInstance;
55
55
 
56
- interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" | "pauseOnHidden" | "initialPhase" | "skeletonColor"> {
57
- /**
58
- * Number of dot columns in the grid.
59
- * @default 32
60
- */
61
- cols?: number;
62
- /**
63
- * Number of dot rows in the grid.
64
- * @default 32
65
- */
66
- rows?: number;
67
- /**
68
- * Controls the corner rounding of each dot.
69
- * `0` renders as a sharp-cornered square,
70
- * `1` renders as a full circle.
71
- * Values between `0` and `1` give rounded rectangles.
72
- * @default 1
73
- */
74
- roundness?: number;
75
- /**
76
- * Number of trail points to keep.
77
- * Larger values mean the trail extends further back from the head.
78
- * @default cols * 3
79
- */
80
- trailLength?: number;
81
- /**
82
- * Color of lit dots. Single color string for solid mode; array of two or more colors for gradient mode.
83
- * Gradient mode samples a color per dot based on its position in the trail (tail → head).
84
- * Background dots always use the first color at 5% opacity.
85
- * @default '#ffffff'
86
- */
87
- trailColor?: TrailColor;
88
- /**
89
- * Trail rendering style.
90
- * - `'default'` — solid color, alpha varies by intensity.
91
- * - `'gradient-static'` — each dot's color is sampled from the `trailColor` gradient. Requires `trailColor` array.
92
- * - `'gradient-animated'` — same as `gradient-static` but the gradient phase shifts over time.
93
- * @default 'default'
94
- */
95
- trailStyle?: TrailStyle;
96
- }
56
+ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
57
+
58
+ /**
59
+ * Creates a Canvas 2D renderer for sarmal animations
60
+ * Renders the skeleton and the trail
61
+ */
62
+ declare function createRenderer(options: RendererOptions): SarmalInstance;
63
+
97
64
  /**
98
65
  * Creates a dot matrix renderer for a sarmal animation on a canvas element.
99
66
  *
@@ -129,14 +96,6 @@ interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" |
129
96
  */
130
97
  declare function createSarmalDotMatrix(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: DotMatrixSarmalOptions): SarmalInstance<DotMatrixRuntimeRenderOptions>;
131
98
 
132
- declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
133
-
134
- /**
135
- * Creates a Canvas 2D renderer for sarmal animations
136
- * Renders the skeleton and the trail
137
- */
138
- declare function createRenderer(options: RendererOptions): SarmalInstance;
139
-
140
99
  /**
141
100
  * Evaluates a closed Catmull-Rom spline through every point in `points`
142
101
  *
@@ -195,4 +154,4 @@ declare function drawCurve(points: Array<ControlPoint>, opts?: {
195
154
  */
196
155
  declare function createSarmal(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: SarmalOptions): SarmalInstance;
197
156
 
198
- export { BaseRendererOptions, CurveDef, DotMatrixRuntimeRenderOptions, type DotMatrixSarmalOptions, Engine, Point, RendererOptions, type SVGRendererOptions, type SVGSarmalOptions, SarmalInstance, SarmalOptions, TrailColor, TrailStyle, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalDotMatrix, createSarmalSVG, drawCurve, evaluateCatmullRom };
157
+ export { BaseRendererOptions, CurveDef, DotMatrixRuntimeRenderOptions, DotMatrixSarmalOptions, Engine, Point, RendererOptions, type SVGRendererOptions, type SVGSarmalOptions, SarmalInstance, SarmalOptions, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalDotMatrix, createSarmalSVG, drawCurve, evaluateCatmullRom };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { B as BaseRendererOptions, E as Engine, S as SarmalInstance, C as CurveDef, T as TrailColor, a as TrailStyle, D as DotMatrixRuntimeRenderOptions, R as RendererOptions, b as ControlPoint, P as Point, c as SarmalOptions } from './types-B1XeFpuq.js';
2
- export { d as BaseRuntimeRenderOptions, J as JumpOptions, M as MorphStrategy, e as RuntimeRenderOptions, f as SeekOptions } from './types-B1XeFpuq.js';
1
+ import { B as BaseRendererOptions, E as Engine, S as SarmalInstance, C as CurveDef, R as RendererOptions, D as DotMatrixSarmalOptions, a as DotMatrixRuntimeRenderOptions, b as ControlPoint, P as Point, c as SarmalOptions } from './types-CmPFR9U3.js';
2
+ export { d as BaseDotMatrixOptions, e as BaseInit, f as BaseRuntimeRenderOptions, g as BaseSarmalOptions, h as CanvasInit, i as DotMatrixInit, J as JumpOptions, M as MorphStrategy, j as RuntimeRenderOptions, k as SeekOptions, T as TrailColor, l as TrailStyle } from './types-CmPFR9U3.js';
3
3
  export { CurveName, curves } from './curves/index.js';
4
- export { B as BoundaryResult, S as SarmalPalette, c as computeBoundaries, p as palettes } from './renderer-shared-DWPVHjKZ.js';
4
+ export { B as BoundaryResult, S as SarmalPalette, c as computeBoundaries, p as palettes } from './renderer-shared-2tEwOWJm.js';
5
5
  export { artemis2 } from './curves/artemis2.js';
6
6
  export { astroid } from './curves/astroid.js';
7
7
  export { deltoid } from './curves/deltoid.js';
@@ -53,47 +53,14 @@ declare function createSVGRenderer(options: SVGRendererOptions): SarmalInstance;
53
53
  */
54
54
  declare function createSarmalSVG(container: SVGSVGElement, curveDef: CurveDef, options?: SVGSarmalOptions): SarmalInstance;
55
55
 
56
- interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" | "pauseOnHidden" | "initialPhase" | "skeletonColor"> {
57
- /**
58
- * Number of dot columns in the grid.
59
- * @default 32
60
- */
61
- cols?: number;
62
- /**
63
- * Number of dot rows in the grid.
64
- * @default 32
65
- */
66
- rows?: number;
67
- /**
68
- * Controls the corner rounding of each dot.
69
- * `0` renders as a sharp-cornered square,
70
- * `1` renders as a full circle.
71
- * Values between `0` and `1` give rounded rectangles.
72
- * @default 1
73
- */
74
- roundness?: number;
75
- /**
76
- * Number of trail points to keep.
77
- * Larger values mean the trail extends further back from the head.
78
- * @default cols * 3
79
- */
80
- trailLength?: number;
81
- /**
82
- * Color of lit dots. Single color string for solid mode; array of two or more colors for gradient mode.
83
- * Gradient mode samples a color per dot based on its position in the trail (tail → head).
84
- * Background dots always use the first color at 5% opacity.
85
- * @default '#ffffff'
86
- */
87
- trailColor?: TrailColor;
88
- /**
89
- * Trail rendering style.
90
- * - `'default'` — solid color, alpha varies by intensity.
91
- * - `'gradient-static'` — each dot's color is sampled from the `trailColor` gradient. Requires `trailColor` array.
92
- * - `'gradient-animated'` — same as `gradient-static` but the gradient phase shifts over time.
93
- * @default 'default'
94
- */
95
- trailStyle?: TrailStyle;
96
- }
56
+ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
57
+
58
+ /**
59
+ * Creates a Canvas 2D renderer for sarmal animations
60
+ * Renders the skeleton and the trail
61
+ */
62
+ declare function createRenderer(options: RendererOptions): SarmalInstance;
63
+
97
64
  /**
98
65
  * Creates a dot matrix renderer for a sarmal animation on a canvas element.
99
66
  *
@@ -129,14 +96,6 @@ interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" |
129
96
  */
130
97
  declare function createSarmalDotMatrix(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: DotMatrixSarmalOptions): SarmalInstance<DotMatrixRuntimeRenderOptions>;
131
98
 
132
- declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
133
-
134
- /**
135
- * Creates a Canvas 2D renderer for sarmal animations
136
- * Renders the skeleton and the trail
137
- */
138
- declare function createRenderer(options: RendererOptions): SarmalInstance;
139
-
140
99
  /**
141
100
  * Evaluates a closed Catmull-Rom spline through every point in `points`
142
101
  *
@@ -195,4 +154,4 @@ declare function drawCurve(points: Array<ControlPoint>, opts?: {
195
154
  */
196
155
  declare function createSarmal(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: SarmalOptions): SarmalInstance;
197
156
 
198
- export { BaseRendererOptions, CurveDef, DotMatrixRuntimeRenderOptions, type DotMatrixSarmalOptions, Engine, Point, RendererOptions, type SVGRendererOptions, type SVGSarmalOptions, SarmalInstance, SarmalOptions, TrailColor, TrailStyle, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalDotMatrix, createSarmalSVG, drawCurve, evaluateCatmullRom };
157
+ export { BaseRendererOptions, CurveDef, DotMatrixRuntimeRenderOptions, DotMatrixSarmalOptions, Engine, Point, RendererOptions, type SVGRendererOptions, type SVGSarmalOptions, SarmalInstance, SarmalOptions, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalDotMatrix, createSarmalSVG, drawCurve, evaluateCatmullRom };
package/dist/index.js CHANGED
@@ -362,15 +362,42 @@ function computeBoundaries(pts, logicalWidth, logicalHeight, minPaddingPx = FIT_
362
362
  offsetY: (logicalHeight - h * scale) / 2 - minY * scale
363
363
  };
364
364
  }
365
- function enginePassthroughs(engine) {
365
+ var DESTROYED_ERROR = "[sarmal] Instance has been destroyed and cannot be used again. Call pause() instead of destroy() for temporary suspension.";
366
+ function enginePassthroughs(engine, isDestroyed) {
367
+ function guard() {
368
+ if (isDestroyed()) {
369
+ throw new Error(DESTROYED_ERROR);
370
+ }
371
+ }
366
372
  return {
367
- jump: engine.jump,
368
- seek: engine.seek,
369
- setSpeed: engine.setSpeed,
370
- getSpeed: engine.getSpeed,
371
- resetSpeed: engine.resetSpeed,
372
- setSpeedOver: engine.setSpeedOver,
373
- getSarmalSkeleton: engine.getSarmalSkeleton
373
+ jump(phase, options) {
374
+ guard();
375
+ engine.jump(phase, options);
376
+ },
377
+ seek(phase, options) {
378
+ guard();
379
+ engine.seek(phase, options);
380
+ },
381
+ setSpeed(speed) {
382
+ guard();
383
+ engine.setSpeed(speed);
384
+ },
385
+ getSpeed() {
386
+ guard();
387
+ return engine.getSpeed();
388
+ },
389
+ resetSpeed() {
390
+ guard();
391
+ engine.resetSpeed();
392
+ },
393
+ setSpeedOver(speed, duration) {
394
+ guard();
395
+ return engine.setSpeedOver(speed, duration);
396
+ },
397
+ getSarmalSkeleton() {
398
+ guard();
399
+ return engine.getSarmalSkeleton();
400
+ }
374
401
  };
375
402
  }
376
403
  var palettes = {
@@ -741,6 +768,7 @@ function createRenderer(options) {
741
768
  let animationId = null;
742
769
  let lastTime = 0;
743
770
  let pausedByVisibility = false;
771
+ let destroyed = false;
744
772
  let morphResolve = null;
745
773
  let morphReject = null;
746
774
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
@@ -912,6 +940,9 @@ function createRenderer(options) {
912
940
  const shouldAutoStart = options.autoStart !== false;
913
941
  const instance = {
914
942
  play() {
943
+ if (destroyed) {
944
+ throw new Error(DESTROYED_ERROR);
945
+ }
915
946
  if (animationId !== null) {
916
947
  return;
917
948
  }
@@ -919,6 +950,9 @@ function createRenderer(options) {
919
950
  loop();
920
951
  },
921
952
  pause() {
953
+ if (destroyed) {
954
+ throw new Error(DESTROYED_ERROR);
955
+ }
922
956
  if (animationId === null) {
923
957
  return;
924
958
  }
@@ -927,24 +961,36 @@ function createRenderer(options) {
927
961
  engine.cancelSpeedTransition();
928
962
  },
929
963
  reset() {
964
+ if (destroyed) {
965
+ throw new Error(DESTROYED_ERROR);
966
+ }
930
967
  engine.reset();
931
968
  trail = [];
932
969
  head = null;
933
970
  },
934
971
  destroy() {
972
+ if (destroyed) {
973
+ return;
974
+ }
975
+ destroyed = true;
935
976
  if (animationId !== null) {
936
977
  cancelAnimationFrame(animationId);
937
978
  animationId = null;
938
979
  }
939
980
  document.removeEventListener("visibilitychange", handleVisibilityChange);
981
+ engine.cancelSpeedTransition();
940
982
  if (morphReject !== null) {
941
- morphReject(new Error("Instance destroyed during morph"));
983
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
942
984
  morphResolve = null;
943
985
  morphReject = null;
944
986
  }
987
+ ctx.clearRect(0, 0, logicalWidth, logicalHeight);
945
988
  },
946
- ...enginePassthroughs(engine),
989
+ ...enginePassthroughs(engine, () => destroyed),
947
990
  morphTo(target, options2) {
991
+ if (destroyed) {
992
+ throw new Error(DESTROYED_ERROR);
993
+ }
948
994
  if (morphResolve !== null) {
949
995
  engine.completeMorph();
950
996
  morphResolve();
@@ -961,6 +1007,9 @@ function createRenderer(options) {
961
1007
  });
962
1008
  },
963
1009
  setRenderOptions(partial) {
1010
+ if (destroyed) {
1011
+ throw new Error(DESTROYED_ERROR);
1012
+ }
964
1013
  validateRenderOptions(partial);
965
1014
  if (partial.trailColor !== void 0) {
966
1015
  trailColor = partial.trailColor;
@@ -1215,6 +1264,7 @@ function createSVGRenderer(options) {
1215
1264
  let animationId = null;
1216
1265
  let lastTime = 0;
1217
1266
  let pausedByVisibility = false;
1267
+ let destroyed = false;
1218
1268
  const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1219
1269
  let morphResolve = null;
1220
1270
  let morphReject = null;
@@ -1283,6 +1333,9 @@ function createSVGRenderer(options) {
1283
1333
  const shouldAutoStart = options.autoStart !== false;
1284
1334
  const instance = {
1285
1335
  play() {
1336
+ if (destroyed) {
1337
+ throw new Error(DESTROYED_ERROR);
1338
+ }
1286
1339
  if (animationId !== null) {
1287
1340
  return;
1288
1341
  }
@@ -1290,6 +1343,9 @@ function createSVGRenderer(options) {
1290
1343
  loop();
1291
1344
  },
1292
1345
  pause() {
1346
+ if (destroyed) {
1347
+ throw new Error(DESTROYED_ERROR);
1348
+ }
1293
1349
  if (animationId === null) {
1294
1350
  return;
1295
1351
  }
@@ -1298,23 +1354,34 @@ function createSVGRenderer(options) {
1298
1354
  engine.cancelSpeedTransition();
1299
1355
  },
1300
1356
  reset() {
1357
+ if (destroyed) {
1358
+ throw new Error(DESTROYED_ERROR);
1359
+ }
1301
1360
  engine.reset();
1302
1361
  },
1303
1362
  destroy() {
1363
+ if (destroyed) {
1364
+ return;
1365
+ }
1366
+ destroyed = true;
1304
1367
  if (animationId !== null) {
1305
1368
  cancelAnimationFrame(animationId);
1306
1369
  animationId = null;
1307
1370
  }
1308
1371
  document.removeEventListener("visibilitychange", handleVisibilityChange);
1372
+ engine.cancelSpeedTransition();
1309
1373
  if (morphReject !== null) {
1310
- morphReject(new Error("Instance destroyed during morph"));
1374
+ morphReject(new Error("[sarmal] Instance destroyed during morph"));
1311
1375
  morphResolve = null;
1312
1376
  morphReject = null;
1313
1377
  }
1314
1378
  group.remove();
1315
1379
  },
1316
- ...enginePassthroughs(engine),
1380
+ ...enginePassthroughs(engine, () => destroyed),
1317
1381
  morphTo(target, options2) {
1382
+ if (destroyed) {
1383
+ throw new Error(DESTROYED_ERROR);
1384
+ }
1318
1385
  if (morphResolve !== null) {
1319
1386
  engine.completeMorph();
1320
1387
  morphResolve();
@@ -1340,6 +1407,9 @@ function createSVGRenderer(options) {
1340
1407
  });
1341
1408
  },
1342
1409
  setRenderOptions(partial) {
1410
+ if (destroyed) {
1411
+ throw new Error(DESTROYED_ERROR);
1412
+ }
1343
1413
  validateRenderOptions(partial);
1344
1414
  const prevTrailStyle = trailStyle;
1345
1415
  if (partial.trailColor !== void 0) {
@@ -1469,6 +1539,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1469
1539
  let animationId = null;
1470
1540
  let lastTime = 0;
1471
1541
  let pausedByVisibility = false;
1542
+ let destroyed = false;
1472
1543
  let morphResolve = null;
1473
1544
  let morphReject = null;
1474
1545
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
@@ -1722,6 +1793,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1722
1793
  const instance = {
1723
1794
  /** Starts the animation loop. Does nothing if already running. */
1724
1795
  play() {
1796
+ if (destroyed) {
1797
+ throw new Error(DESTROYED_ERROR);
1798
+ }
1725
1799
  if (animationId !== null) {
1726
1800
  return;
1727
1801
  }
@@ -1730,6 +1804,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1730
1804
  },
1731
1805
  /** Pauses the animation loop. Preserves current trail state. */
1732
1806
  pause() {
1807
+ if (destroyed) {
1808
+ throw new Error(DESTROYED_ERROR);
1809
+ }
1733
1810
  if (animationId === null) {
1734
1811
  return;
1735
1812
  }
@@ -1739,29 +1816,46 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1739
1816
  },
1740
1817
  /** Resets the animation to the start of the curve and clears the grid. */
1741
1818
  reset() {
1819
+ if (destroyed) {
1820
+ throw new Error(DESTROYED_ERROR);
1821
+ }
1742
1822
  engine.reset();
1743
1823
  grid.fill(0);
1744
1824
  },
1745
- /** Stops the animation and removes all event listeners. */
1825
+ /**
1826
+ * Permanently stops the animation and clears the visual output.
1827
+ * Calling any method on a destroyed instance throws an error.
1828
+ * `destroy()` is idempotent — calling it multiple times is safe.
1829
+ */
1746
1830
  destroy() {
1831
+ if (destroyed) {
1832
+ return;
1833
+ }
1834
+ destroyed = true;
1747
1835
  if (animationId !== null) {
1748
1836
  cancelAnimationFrame(animationId);
1749
1837
  animationId = null;
1750
1838
  }
1751
1839
  document.removeEventListener("visibilitychange", handleVisibilityChange);
1840
+ engine.cancelSpeedTransition();
1752
1841
  if (morphReject !== null) {
1753
1842
  morphReject(new Error("[sarmal] Instance destroyed during morph"));
1754
1843
  morphResolve = null;
1755
1844
  morphReject = null;
1756
1845
  }
1846
+ grid.fill(0);
1847
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1757
1848
  },
1758
- ...enginePassthroughs(engine),
1849
+ ...enginePassthroughs(engine, () => destroyed),
1759
1850
  /**
1760
1851
  * Smoothly transitions from the current curve to `target`.
1761
1852
  * If a morph is already in progress, it is snapped to completion before the new one starts.
1762
1853
  * @returns A Promise that resolves when the transition finishes.
1763
1854
  */
1764
1855
  morphTo(target, opts) {
1856
+ if (destroyed) {
1857
+ throw new Error(DESTROYED_ERROR);
1858
+ }
1765
1859
  if (morphResolve !== null) {
1766
1860
  completeMorphNow();
1767
1861
  }
@@ -1781,6 +1875,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1781
1875
  * ! Validation fails the entire call if any field is invalid, leaving options unchanged.
1782
1876
  */
1783
1877
  setRenderOptions(partial) {
1878
+ if (destroyed) {
1879
+ throw new Error(DESTROYED_ERROR);
1880
+ }
1784
1881
  validateBaseRenderOptions(partial);
1785
1882
  let needsRebuildBg = false;
1786
1883
  if (partial.trailColor !== void 0) {