@sarmal/core 0.18.0 → 0.20.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/index.d.cts CHANGED
@@ -4,11 +4,11 @@ import {
4
4
  S as SarmalInstance,
5
5
  C as CurveDef,
6
6
  R as RendererOptions,
7
+ P as Point,
7
8
  a as SarmalOptions,
8
9
  } from "./types-frtEoAq6.cjs";
9
10
  export {
10
11
  J as JumpOptions,
11
- P as Point,
12
12
  b as RuntimeRenderOptions,
13
13
  c as SeekOptions,
14
14
  T as TrailColor,
@@ -31,8 +31,8 @@ import "./curves/star4.cjs";
31
31
  import "./curves/star7.cjs";
32
32
 
33
33
  interface SVGRendererOptions extends BaseRendererOptions {
34
- /** Container element that will contain the SVG */
35
- container: Element;
34
+ /** SVG element the renderer draws into directly */
35
+ container: SVGSVGElement;
36
36
  engine: Engine;
37
37
  /** @default 'Loading' */
38
38
  ariaLabel?: string;
@@ -47,22 +47,25 @@ interface SVGSarmalOptions extends Omit<SVGRendererOptions, "container" | "engin
47
47
  */
48
48
  declare function createSVGRenderer(options: SVGRendererOptions): SarmalInstance;
49
49
  /**
50
- * Creates a sarmal animation inside a container element using an SVG renderer
51
- * The SVG is appended to the container and animated via requestAnimationFrame
50
+ * Creates a sarmal animation directly inside an `<svg>` element using an SVG renderer.
51
+ * The passed `<svg>` element is set to `viewBox="0 0 100 100"` and animated via requestAnimationFrame
52
52
  *
53
53
  * @example
54
54
  * ```ts
55
55
  * import { createSarmalSVG, epitrochoid7 } from '@sarmal/core'
56
- * const sarmal = createSarmalSVG(document.getElementById('spinner'), epitrochoid7)
56
+ *
57
+ * // <svg id="spinner"></svg> in your HTML
58
+ * const svg = document.getElementById('spinner')
59
+ * const sarmal = createSarmalSVG(svg, epitrochoid7)
57
60
  *
58
61
  * // To control manually, use autoStart: false
59
- * const controlled = createSarmalSVG(container, rose5, { autoStart: false })
62
+ * const controlled = createSarmalSVG(svg, rose5, { autoStart: false })
60
63
  * controlled.play() // Start when ready
61
64
  * controlled.pause() // Pause later
62
65
  * ```
63
66
  */
64
67
  declare function createSarmalSVG(
65
- container: Element,
68
+ container: SVGSVGElement,
66
69
  curveDef: CurveDef,
67
70
  options?: SVGSarmalOptions,
68
71
  ): SarmalInstance;
@@ -89,6 +92,45 @@ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
89
92
  */
90
93
  declare function createRenderer(options: RendererOptions): SarmalInstance;
91
94
 
95
+ /**
96
+ * Evaluates a closed Catmull-Rom spline through every point in `points`
97
+ *
98
+ * The spline treats `points` as a **closed loop**
99
+ * The last point wraps back to the first,
100
+ * and each segment uses a phantom predecessor / successor so the
101
+ * curve passes exactly through every control point.
102
+ *
103
+ * @param points At least 1 point. An empty array yields `(0, 0)`. A single point returns that point for every `t`
104
+ * @param t Parametric position along the closed loop. Wraps into `[0, 2π)` automatically, so values outside that range are remapped rather than rejected
105
+ * @returns The `(x, y)` position on the spline at time `t`
106
+ */
107
+ declare function evaluateCatmullRom(points: Array<[number, number]>, t: number): Point;
108
+ /**
109
+ * The returned curve definition produces a closed Catmull-Rom spline that
110
+ * passes through every point in order, looping back from the last point to the first.
111
+ *
112
+ * @param points Array of control points in **normalized `[−1, 1]` space**,
113
+ * matching the playground's draw-mode coordinate system.
114
+ * ! Must contain at least 3 points.
115
+ * @returns A `CurveDef` with `period: 2π` and the spline evaluator as its `fn`.
116
+ * `name` is set to `"custom"`.
117
+ * @throws If `points` has fewer than 3 entries.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * import { createSarmal, drawCurve } from '@sarmal/core'
122
+ *
123
+ * const curve = drawCurve([
124
+ * [-0.5, 0.3],
125
+ * [ 0.2, -0.8],
126
+ * [ 0.7, 0.4],
127
+ * ])
128
+ *
129
+ * createSarmal(canvas, curve)
130
+ * ```
131
+ */
132
+ declare function drawCurve(points: Array<[number, number]>): CurveDef;
133
+
92
134
  /**
93
135
  * Creates a sarmal animation on a canvas element
94
136
  *
@@ -113,6 +155,7 @@ export {
113
155
  BaseRendererOptions,
114
156
  CurveDef,
115
157
  Engine,
158
+ Point,
116
159
  RendererOptions,
117
160
  type SVGRendererOptions,
118
161
  type SVGSarmalOptions,
@@ -124,5 +167,7 @@ export {
124
167
  createSVGRenderer,
125
168
  createSarmal,
126
169
  createSarmalSVG,
170
+ drawCurve,
171
+ evaluateCatmullRom,
127
172
  palettes,
128
173
  };
package/dist/index.d.ts CHANGED
@@ -4,11 +4,11 @@ import {
4
4
  S as SarmalInstance,
5
5
  C as CurveDef,
6
6
  R as RendererOptions,
7
+ P as Point,
7
8
  a as SarmalOptions,
8
9
  } from "./types-frtEoAq6.js";
9
10
  export {
10
11
  J as JumpOptions,
11
- P as Point,
12
12
  b as RuntimeRenderOptions,
13
13
  c as SeekOptions,
14
14
  T as TrailColor,
@@ -31,8 +31,8 @@ import "./curves/star4.js";
31
31
  import "./curves/star7.js";
32
32
 
33
33
  interface SVGRendererOptions extends BaseRendererOptions {
34
- /** Container element that will contain the SVG */
35
- container: Element;
34
+ /** SVG element the renderer draws into directly */
35
+ container: SVGSVGElement;
36
36
  engine: Engine;
37
37
  /** @default 'Loading' */
38
38
  ariaLabel?: string;
@@ -47,22 +47,25 @@ interface SVGSarmalOptions extends Omit<SVGRendererOptions, "container" | "engin
47
47
  */
48
48
  declare function createSVGRenderer(options: SVGRendererOptions): SarmalInstance;
49
49
  /**
50
- * Creates a sarmal animation inside a container element using an SVG renderer
51
- * The SVG is appended to the container and animated via requestAnimationFrame
50
+ * Creates a sarmal animation directly inside an `<svg>` element using an SVG renderer.
51
+ * The passed `<svg>` element is set to `viewBox="0 0 100 100"` and animated via requestAnimationFrame
52
52
  *
53
53
  * @example
54
54
  * ```ts
55
55
  * import { createSarmalSVG, epitrochoid7 } from '@sarmal/core'
56
- * const sarmal = createSarmalSVG(document.getElementById('spinner'), epitrochoid7)
56
+ *
57
+ * // <svg id="spinner"></svg> in your HTML
58
+ * const svg = document.getElementById('spinner')
59
+ * const sarmal = createSarmalSVG(svg, epitrochoid7)
57
60
  *
58
61
  * // To control manually, use autoStart: false
59
- * const controlled = createSarmalSVG(container, rose5, { autoStart: false })
62
+ * const controlled = createSarmalSVG(svg, rose5, { autoStart: false })
60
63
  * controlled.play() // Start when ready
61
64
  * controlled.pause() // Pause later
62
65
  * ```
63
66
  */
64
67
  declare function createSarmalSVG(
65
- container: Element,
68
+ container: SVGSVGElement,
66
69
  curveDef: CurveDef,
67
70
  options?: SVGSarmalOptions,
68
71
  ): SarmalInstance;
@@ -89,6 +92,45 @@ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
89
92
  */
90
93
  declare function createRenderer(options: RendererOptions): SarmalInstance;
91
94
 
95
+ /**
96
+ * Evaluates a closed Catmull-Rom spline through every point in `points`
97
+ *
98
+ * The spline treats `points` as a **closed loop**
99
+ * The last point wraps back to the first,
100
+ * and each segment uses a phantom predecessor / successor so the
101
+ * curve passes exactly through every control point.
102
+ *
103
+ * @param points At least 1 point. An empty array yields `(0, 0)`. A single point returns that point for every `t`
104
+ * @param t Parametric position along the closed loop. Wraps into `[0, 2π)` automatically, so values outside that range are remapped rather than rejected
105
+ * @returns The `(x, y)` position on the spline at time `t`
106
+ */
107
+ declare function evaluateCatmullRom(points: Array<[number, number]>, t: number): Point;
108
+ /**
109
+ * The returned curve definition produces a closed Catmull-Rom spline that
110
+ * passes through every point in order, looping back from the last point to the first.
111
+ *
112
+ * @param points Array of control points in **normalized `[−1, 1]` space**,
113
+ * matching the playground's draw-mode coordinate system.
114
+ * ! Must contain at least 3 points.
115
+ * @returns A `CurveDef` with `period: 2π` and the spline evaluator as its `fn`.
116
+ * `name` is set to `"custom"`.
117
+ * @throws If `points` has fewer than 3 entries.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * import { createSarmal, drawCurve } from '@sarmal/core'
122
+ *
123
+ * const curve = drawCurve([
124
+ * [-0.5, 0.3],
125
+ * [ 0.2, -0.8],
126
+ * [ 0.7, 0.4],
127
+ * ])
128
+ *
129
+ * createSarmal(canvas, curve)
130
+ * ```
131
+ */
132
+ declare function drawCurve(points: Array<[number, number]>): CurveDef;
133
+
92
134
  /**
93
135
  * Creates a sarmal animation on a canvas element
94
136
  *
@@ -113,6 +155,7 @@ export {
113
155
  BaseRendererOptions,
114
156
  CurveDef,
115
157
  Engine,
158
+ Point,
116
159
  RendererOptions,
117
160
  type SVGRendererOptions,
118
161
  type SVGSarmalOptions,
@@ -124,5 +167,7 @@ export {
124
167
  createSVGRenderer,
125
168
  createSarmal,
126
169
  createSarmalSVG,
170
+ drawCurve,
171
+ evaluateCatmullRom,
127
172
  palettes,
128
173
  };
package/dist/index.js CHANGED
@@ -298,12 +298,20 @@ function computeNormal(trail, i) {
298
298
  const tangent = computeTangent(trail, i);
299
299
  return { x: -tangent.y, y: tangent.x };
300
300
  }
301
- function computeTrailQuad(trail, i, trailCount, toX, toY) {
301
+ function computeTrailQuad(
302
+ trail,
303
+ i,
304
+ trailCount,
305
+ toX,
306
+ toY,
307
+ minWidth = TRAIL_MIN_WIDTH,
308
+ maxWidth = TRAIL_MAX_WIDTH,
309
+ ) {
302
310
  const progress = i / (trailCount - 1);
303
311
  const nextProgress = (i + 1) / (trailCount - 1);
304
312
  const opacity = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
305
- const w0 = (TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
306
- const w1 = (TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
313
+ const w0 = (minWidth + progress * (maxWidth - minWidth)) / 2;
314
+ const w1 = (minWidth + nextProgress * (maxWidth - minWidth)) / 2;
307
315
  const curr = trail[i];
308
316
  const next = trail[i + 1];
309
317
  const n0 = computeNormal(trail, i);
@@ -867,62 +875,61 @@ function createSVGRenderer(options) {
867
875
  let trailPalette = resolveTrailPalette(trailColor);
868
876
  const ariaLabel = options.ariaLabel ?? "Loading";
869
877
  warnIfTrailColorMismatch(trailColor, trailStyle);
870
- const htmlContainer = container;
871
- const width = htmlContainer.offsetWidth || 200;
872
- const height = htmlContainer.offsetHeight || 200;
873
- const headRadius = options.headRadius ?? getHeadDotRadius(width, height);
874
- const svg = el("svg");
875
- svg.setAttribute("width", String(width));
876
- svg.setAttribute("height", String(height));
877
- svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
878
- svg.setAttribute("role", "img");
879
- svg.setAttribute("aria-label", ariaLabel);
878
+ const viewSize = 100;
879
+ const headRadius = options.headRadius ?? 1.5;
880
+ const svgTrailMinWidth = 0.25;
881
+ const svgTrailMaxWidth = 1.25;
882
+ const svgSkeletonStrokeWidth = "0.75";
883
+ container.setAttribute("viewBox", `0 0 ${viewSize} ${viewSize}`);
884
+ container.setAttribute("role", "img");
885
+ container.setAttribute("aria-label", ariaLabel);
886
+ const group = el("g");
880
887
  const titleEl = el("title");
881
888
  titleEl.textContent = ariaLabel;
882
- svg.appendChild(titleEl);
889
+ group.appendChild(titleEl);
883
890
  const skeletonPath = el("path");
884
891
  skeletonPath.setAttribute("data-sarmal-role", "skeleton");
885
892
  skeletonPath.setAttribute("fill", "none");
886
893
  skeletonPath.setAttribute("stroke", skeletonColor);
887
894
  skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
888
- skeletonPath.setAttribute("stroke-width", "1.5");
895
+ skeletonPath.setAttribute("stroke-width", svgSkeletonStrokeWidth);
889
896
  if (skeletonColor === "transparent") {
890
897
  skeletonPath.setAttribute("visibility", "hidden");
891
898
  }
892
- svg.appendChild(skeletonPath);
899
+ group.appendChild(skeletonPath);
893
900
  const skeletonPathA = el("path");
894
901
  skeletonPathA.setAttribute("fill", "none");
895
902
  skeletonPathA.setAttribute("stroke", skeletonColor);
896
- skeletonPathA.setAttribute("stroke-width", "1.5");
903
+ skeletonPathA.setAttribute("stroke-width", svgSkeletonStrokeWidth);
897
904
  skeletonPathA.setAttribute("visibility", "hidden");
898
- svg.appendChild(skeletonPathA);
905
+ group.appendChild(skeletonPathA);
899
906
  const skeletonPathB = el("path");
900
907
  skeletonPathB.setAttribute("fill", "none");
901
908
  skeletonPathB.setAttribute("stroke", skeletonColor);
902
- skeletonPathB.setAttribute("stroke-width", "1.5");
909
+ skeletonPathB.setAttribute("stroke-width", svgSkeletonStrokeWidth);
903
910
  skeletonPathB.setAttribute("visibility", "hidden");
904
- svg.appendChild(skeletonPathB);
911
+ group.appendChild(skeletonPathB);
905
912
  let morphPathABuilt = "";
906
913
  let morphPathBBuilt = "";
907
914
  const trailPaths = [];
908
915
  for (let i = 0; i < poolSize; i++) {
909
916
  const path = el("path");
910
917
  path.setAttribute("fill", trailSolid);
911
- svg.appendChild(path);
918
+ group.appendChild(path);
912
919
  trailPaths.push(path);
913
920
  }
914
921
  const headCircle = el("circle");
915
922
  headCircle.setAttribute("data-sarmal-role", "head");
916
923
  headCircle.setAttribute("fill", headColor);
917
924
  headCircle.setAttribute("r", String(headRadius));
918
- svg.appendChild(headCircle);
919
- container.appendChild(svg);
925
+ group.appendChild(headCircle);
926
+ container.appendChild(group);
920
927
  let gradientAnimTime = 0;
921
928
  let scale = 1;
922
929
  let offsetX = 0;
923
930
  let offsetY = 0;
924
931
  function applyBoundaries(skeleton2) {
925
- const b = computeBoundaries(skeleton2, width, height);
932
+ const b = computeBoundaries(skeleton2, viewSize, viewSize);
926
933
  if (b) {
927
934
  scale = b.scale;
928
935
  offsetX = b.offsetX;
@@ -958,6 +965,8 @@ function createSVGRenderer(options) {
958
965
  trailCount,
959
966
  px,
960
967
  py,
968
+ svgTrailMinWidth,
969
+ svgTrailMaxWidth,
961
970
  );
962
971
  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`;
963
972
  trailPaths[i].setAttribute("d", d);
@@ -1080,7 +1089,7 @@ function createSVGRenderer(options) {
1080
1089
  morphResolve = null;
1081
1090
  morphReject = null;
1082
1091
  }
1083
- svg.remove();
1092
+ group.remove();
1084
1093
  },
1085
1094
  ...enginePassthroughs(engine),
1086
1095
  morphTo(target, options2) {
@@ -1433,6 +1442,55 @@ var curves = {
1433
1442
  lame,
1434
1443
  };
1435
1444
 
1445
+ // src/catmull-rom.ts
1446
+ var PERIOD = 2 * Math.PI;
1447
+ function catmullRom1D(p0, p1, p2, p3, u) {
1448
+ const u2 = u * u;
1449
+ const u3 = u2 * u;
1450
+ return (
1451
+ 0.5 *
1452
+ (2 * p1 +
1453
+ (-p0 + p2) * u +
1454
+ (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 +
1455
+ (-p0 + 3 * p1 - 3 * p2 + p3) * u3)
1456
+ );
1457
+ }
1458
+ function evaluateCatmullRom(points, t) {
1459
+ const N = points.length;
1460
+ if (N === 0) {
1461
+ return { x: 0, y: 0 };
1462
+ }
1463
+ if (N === 1) {
1464
+ return { x: points[0][0], y: points[0][1] };
1465
+ }
1466
+ t = ((t % PERIOD) + PERIOD) % PERIOD;
1467
+ const segmentSize = PERIOD / N;
1468
+ let i = Math.floor(t / segmentSize);
1469
+ if (i >= N) {
1470
+ i = N - 1;
1471
+ }
1472
+ let u = (t - i * segmentSize) / segmentSize;
1473
+ u = Math.max(0, Math.min(1, u));
1474
+ const p0 = points[(i - 1 + N) % N];
1475
+ const p1 = points[i];
1476
+ const p2 = points[(i + 1) % N];
1477
+ const p3 = points[(i + 2) % N];
1478
+ return {
1479
+ x: catmullRom1D(p0[0], p1[0], p2[0], p3[0], u),
1480
+ y: catmullRom1D(p0[1], p1[1], p2[1], p3[1], u),
1481
+ };
1482
+ }
1483
+ function drawCurve(points) {
1484
+ if (points.length < 3) {
1485
+ throw new Error(`drawCurve requires at least 3 points, received ${points.length}.`);
1486
+ }
1487
+ return {
1488
+ name: "custom",
1489
+ fn: (t) => evaluateCatmullRom(points, t),
1490
+ period: PERIOD,
1491
+ };
1492
+ }
1493
+
1436
1494
  // src/index.ts
1437
1495
  function createSarmal(canvas, curveDef, options) {
1438
1496
  const { trailLength, ...rendererOpts } = options ?? {};
@@ -1450,8 +1508,10 @@ export {
1450
1508
  createSarmalSVG,
1451
1509
  curves,
1452
1510
  deltoid,
1511
+ drawCurve,
1453
1512
  epicycloid3,
1454
1513
  epitrochoid7,
1514
+ evaluateCatmullRom,
1455
1515
  lame,
1456
1516
  lissajous32,
1457
1517
  lissajous43,