@sarmal/core 0.9.7 → 0.10.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 (61) hide show
  1. package/dist/auto-init.cjs +171 -166
  2. package/dist/auto-init.cjs.map +1 -1
  3. package/dist/auto-init.d.cts +2 -1
  4. package/dist/auto-init.d.ts +2 -1
  5. package/dist/auto-init.js +170 -165
  6. package/dist/auto-init.js.map +1 -1
  7. package/dist/curves/artemis2.cjs +7 -10
  8. package/dist/curves/artemis2.d.cts +1 -1
  9. package/dist/curves/artemis2.d.ts +1 -1
  10. package/dist/curves/artemis2.js +6 -9
  11. package/dist/curves/astroid.cjs +4 -4
  12. package/dist/curves/astroid.d.cts +1 -1
  13. package/dist/curves/astroid.d.ts +1 -1
  14. package/dist/curves/astroid.js +3 -3
  15. package/dist/curves/deltoid.cjs +4 -4
  16. package/dist/curves/deltoid.d.cts +1 -1
  17. package/dist/curves/deltoid.d.ts +1 -1
  18. package/dist/curves/deltoid.js +3 -3
  19. package/dist/curves/epicycloid3.cjs +4 -4
  20. package/dist/curves/epicycloid3.d.cts +1 -1
  21. package/dist/curves/epicycloid3.d.ts +1 -1
  22. package/dist/curves/epicycloid3.js +3 -3
  23. package/dist/curves/epitrochoid7.cjs +5 -5
  24. package/dist/curves/epitrochoid7.d.cts +1 -1
  25. package/dist/curves/epitrochoid7.d.ts +1 -1
  26. package/dist/curves/epitrochoid7.js +4 -4
  27. package/dist/curves/index.cjs +28 -32
  28. package/dist/curves/index.d.cts +11 -11
  29. package/dist/curves/index.d.ts +11 -11
  30. package/dist/curves/index.js +28 -44
  31. package/dist/curves/lame.cjs +5 -6
  32. package/dist/curves/lame.d.cts +1 -1
  33. package/dist/curves/lame.d.ts +1 -1
  34. package/dist/curves/lame.js +4 -5
  35. package/dist/curves/lissajous32.cjs +4 -4
  36. package/dist/curves/lissajous32.d.cts +1 -1
  37. package/dist/curves/lissajous32.d.ts +1 -1
  38. package/dist/curves/lissajous32.js +3 -3
  39. package/dist/curves/lissajous43.cjs +4 -4
  40. package/dist/curves/lissajous43.d.cts +1 -1
  41. package/dist/curves/lissajous43.d.ts +1 -1
  42. package/dist/curves/lissajous43.js +3 -3
  43. package/dist/curves/rose3.cjs +4 -4
  44. package/dist/curves/rose3.d.cts +1 -1
  45. package/dist/curves/rose3.d.ts +1 -1
  46. package/dist/curves/rose3.js +3 -3
  47. package/dist/curves/rose5.cjs +4 -4
  48. package/dist/curves/rose5.d.cts +1 -1
  49. package/dist/curves/rose5.d.ts +1 -1
  50. package/dist/curves/rose5.js +3 -3
  51. package/dist/index.cjs +211 -283
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +31 -64
  54. package/dist/index.d.ts +31 -64
  55. package/dist/index.js +211 -300
  56. package/dist/index.js.map +1 -1
  57. package/dist/types-DcyISvnH.d.cts +230 -0
  58. package/dist/types-DcyISvnH.d.ts +230 -0
  59. package/package.json +2 -1
  60. package/dist/types-cR2xOewv.d.cts +0 -234
  61. package/dist/types-cR2xOewv.d.ts +0 -234
package/dist/index.js CHANGED
@@ -61,13 +61,13 @@ function resolveCurve(curveDef) {
61
61
  period,
62
62
  speed,
63
63
  skeleton: curveDef.skeleton,
64
- skeletonFn: curveDef.skeletonFn,
64
+ skeletonFn: curveDef.skeletonFn
65
65
  };
66
66
  }
67
67
  function createEngine(curveDef, trailLength = 120) {
68
68
  if (!Number.isFinite(trailLength) || trailLength <= 0) {
69
69
  throw new RangeError(
70
- `[sarmal] trailLength must be a positive finite number, got ${trailLength}`,
70
+ `[sarmal] trailLength must be a positive finite number, got ${trailLength}`
71
71
  );
72
72
  }
73
73
  let curve = resolveCurve(curveDef);
@@ -96,7 +96,7 @@ function createEngine(curveDef, trailLength = 120) {
96
96
  actualTime += deltaTime;
97
97
  if (morphCurveB !== null && _morphAlpha !== null) {
98
98
  const a = curve.fn(t, actualTime, EMPTY_PARAMS);
99
- const tB = _morphStrategy === "normalized" ? (t / curve.period) * morphCurveB.period : t;
99
+ const tB = _morphStrategy === "normalized" ? t / curve.period * morphCurveB.period : t;
100
100
  const b = morphCurveB.fn(tB, actualTime, EMPTY_PARAMS);
101
101
  trail.push(a.x + (b.x - a.x) * _morphAlpha, a.y + (b.y - a.y) * _morphAlpha);
102
102
  } else {
@@ -119,15 +119,15 @@ function createEngine(curveDef, trailLength = 120) {
119
119
  actualTime = 0;
120
120
  trail.clear();
121
121
  },
122
- seek(newT, { clearTrail = false } = {}) {
123
- t = ((newT % curve.period) + curve.period) % curve.period;
122
+ jump(newT, { clearTrail = false } = {}) {
123
+ t = (newT % curve.period + curve.period) % curve.period;
124
124
  if (clearTrail) {
125
125
  trail.clear();
126
126
  }
127
127
  },
128
- seekWithTrail(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
128
+ seek(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
129
129
  const advance = curve.speed * step;
130
- const target = ((targetT % curve.period) + curve.period) % curve.period;
130
+ const target = (targetT % curve.period + curve.period) % curve.period;
131
131
  const targetTime = target / curve.speed;
132
132
  t = target;
133
133
  actualTime = targetTime;
@@ -136,7 +136,7 @@ function createEngine(curveDef, trailLength = 120) {
136
136
  const count = wrap ? trailLength : Math.min(trailLength, pointsFromStart);
137
137
  for (let i = count - 1; i >= 0; i--) {
138
138
  const sampleT = target - i * advance;
139
- const wrappedT = ((sampleT % curve.period) + curve.period) % curve.period;
139
+ const wrappedT = (sampleT % curve.period + curve.period) % curve.period;
140
140
  const time = targetTime - i * step;
141
141
  const point = curve.fn(wrappedT, time, EMPTY_PARAMS);
142
142
  trail.push(point.x, point.y);
@@ -153,16 +153,13 @@ function createEngine(curveDef, trailLength = 120) {
153
153
  ...frozenB,
154
154
  fn: (sampleT, time, params) => {
155
155
  const a = frozenA.fn(sampleT, time, params);
156
- const tB =
157
- frozenStrategy === "normalized"
158
- ? (sampleT / frozenA.period) * frozenB.period
159
- : sampleT;
156
+ const tB = frozenStrategy === "normalized" ? sampleT / frozenA.period * frozenB.period : sampleT;
160
157
  const b = frozenB.fn(tB, time, params);
161
158
  return {
162
159
  x: a.x + (b.x - a.x) * frozenAlpha,
163
- y: a.y + (b.y - a.y) * frozenAlpha,
160
+ y: a.y + (b.y - a.y) * frozenAlpha
164
161
  };
165
- },
162
+ }
166
163
  };
167
164
  }
168
165
  _morphStrategy = strategy;
@@ -175,7 +172,7 @@ function createEngine(curveDef, trailLength = 120) {
175
172
  completeMorph() {
176
173
  if (morphCurveB !== null) {
177
174
  if (_morphStrategy === "normalized" && curve.period !== morphCurveB.period) {
178
- t = (t / curve.period) * morphCurveB.period;
175
+ t = t / curve.period * morphCurveB.period;
179
176
  }
180
177
  curve = morphCurveB;
181
178
  }
@@ -187,46 +184,132 @@ function createEngine(curveDef, trailLength = 120) {
187
184
  const points = new Array(steps);
188
185
  if (morphCurveB !== null && _morphAlpha !== null) {
189
186
  for (let i = 0; i < steps; i++) {
190
- const sampleT = (i / (steps - 1)) * curve.period;
187
+ const sampleT = i / (steps - 1) * curve.period;
191
188
  const a = sampleSkeleton(curve, sampleT);
192
- const tB =
193
- _morphStrategy === "normalized"
194
- ? (sampleT / curve.period) * morphCurveB.period
195
- : sampleT;
189
+ const tB = _morphStrategy === "normalized" ? sampleT / curve.period * morphCurveB.period : sampleT;
196
190
  const b = sampleSkeleton(morphCurveB, tB);
197
191
  points[i] = {
198
192
  x: a.x + (b.x - a.x) * _morphAlpha,
199
- y: a.y + (b.y - a.y) * _morphAlpha,
193
+ y: a.y + (b.y - a.y) * _morphAlpha
200
194
  };
201
195
  }
202
196
  return points;
203
197
  }
204
198
  for (let i = 0; i < steps; i++) {
205
- const sampleT = (i / (steps - 1)) * curve.period;
199
+ const sampleT = i / (steps - 1) * curve.period;
206
200
  points[i] = sampleSkeleton(curve, sampleT);
207
201
  }
208
202
  return points;
209
- },
203
+ }
210
204
  };
211
205
  }
212
206
 
213
- // src/renderer.ts
207
+ // src/renderer-shared.ts
214
208
  var DEFAULT_MORPH_DURATION_MS = 300;
215
- var DEFAULT_HEAD_RADIUS = 4;
216
- var DEFAULT_SKELETON_COLOR = "#ffffff";
217
209
  var DEFAULT_SKELETON_OPACITY = 0.15;
218
210
  var FIT_PADDING = 0.1;
219
211
  var TRAIL_FADE_CURVE = 1.5;
220
212
  var TRAIL_MAX_OPACITY = 0.88;
221
213
  var TRAIL_MIN_WIDTH = 0.5;
222
214
  var TRAIL_MAX_WIDTH = 2.5;
215
+ function computeTangent(trail, i) {
216
+ const count = trail.length;
217
+ if (count < 2) {
218
+ return { x: 1, y: 0 };
219
+ }
220
+ if (i === 0) {
221
+ const dx2 = trail[1].x - trail[0].x;
222
+ const dy2 = trail[1].y - trail[0].y;
223
+ const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
224
+ return { x: dx2 / len2, y: dy2 / len2 };
225
+ }
226
+ if (i === count - 1) {
227
+ const dx2 = trail[count - 1].x - trail[count - 2].x;
228
+ const dy2 = trail[count - 1].y - trail[count - 2].y;
229
+ const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
230
+ return { x: dx2 / len2, y: dy2 / len2 };
231
+ }
232
+ const dx = trail[i + 1].x - trail[i - 1].x;
233
+ const dy = trail[i + 1].y - trail[i - 1].y;
234
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
235
+ return { x: dx / len, y: dy / len };
236
+ }
237
+ function computeNormal(trail, i) {
238
+ const tangent = computeTangent(trail, i);
239
+ return { x: -tangent.y, y: tangent.x };
240
+ }
241
+ function computeTrailQuad(trail, i, trailCount, toX, toY) {
242
+ const progress = i / (trailCount - 1);
243
+ const nextProgress = (i + 1) / (trailCount - 1);
244
+ const opacity = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
245
+ const w0 = (TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
246
+ const w1 = (TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
247
+ const curr = trail[i];
248
+ const next = trail[i + 1];
249
+ const n0 = computeNormal(trail, i);
250
+ const n1 = computeNormal(trail, i + 1);
251
+ const cx = toX(curr);
252
+ const cy = toY(curr);
253
+ const nx = toX(next);
254
+ const ny = toY(next);
255
+ return {
256
+ l0x: cx + n0.x * w0,
257
+ l0y: cy + n0.y * w0,
258
+ r0x: cx - n0.x * w0,
259
+ r0y: cy - n0.y * w0,
260
+ l1x: nx + n1.x * w1,
261
+ l1y: ny + n1.y * w1,
262
+ r1x: nx - n1.x * w1,
263
+ r1y: ny - n1.y * w1,
264
+ opacity,
265
+ progress
266
+ };
267
+ }
268
+ function computeBoundaries(pts, logicalWidth, logicalHeight) {
269
+ if (pts.length === 0) return null;
270
+ const first = pts[0];
271
+ let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
272
+ for (const p of pts) {
273
+ if (p.x < minX) {
274
+ minX = p.x;
275
+ }
276
+ if (p.x > maxX) {
277
+ maxX = p.x;
278
+ }
279
+ if (p.y < minY) {
280
+ minY = p.y;
281
+ }
282
+ if (p.y > maxY) {
283
+ maxY = p.y;
284
+ }
285
+ }
286
+ const w = maxX - minX;
287
+ const h = maxY - minY;
288
+ if (w === 0 && h === 0) {
289
+ throw new Error(
290
+ "[sarmal] Degenerate curve: all skeleton points are identical. Check that your curve fn returns distinct points for different values of t."
291
+ );
292
+ }
293
+ const scaleX = logicalWidth / (w * (1 + FIT_PADDING * 2));
294
+ const scaleY = logicalHeight / (h * (1 + FIT_PADDING * 2));
295
+ const scale = Math.min(scaleX, scaleY);
296
+ return {
297
+ scale,
298
+ offsetX: (logicalWidth - w * scale) / 2 - minX * scale,
299
+ offsetY: (logicalHeight - h * scale) / 2 - minY * scale
300
+ };
301
+ }
302
+
303
+ // src/renderer.ts
304
+ var DEFAULT_HEAD_RADIUS = 4;
305
+ var DEFAULT_SKELETON_COLOR = "#ffffff";
223
306
  var GRADIENT = {
224
307
  bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
225
308
  sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
226
309
  ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
227
310
  ice: ["#1e3a8a", "#67e8f9"],
228
311
  fire: ["#7f1d1d", "#fbbf24"],
229
- forest: ["#14532d", "#86efac"],
312
+ forest: ["#14532d", "#86efac"]
230
313
  };
231
314
  var PRESETS = {
232
315
  bard: GRADIENT.bard,
@@ -234,16 +317,16 @@ var PRESETS = {
234
317
  ocean: GRADIENT.ocean,
235
318
  ice: GRADIENT.ice,
236
319
  fire: GRADIENT.fire,
237
- forest: GRADIENT.forest,
320
+ forest: GRADIENT.forest
238
321
  };
239
322
  function hexToRgb(hex) {
240
323
  const n = parseInt(hex.slice(1), 16);
241
- return { r: n >> 16, g: (n >> 8) & 255, b: n & 255 };
324
+ return { r: n >> 16, g: n >> 8 & 255, b: n & 255 };
242
325
  }
243
326
  var lerpRgb = (a, b, t) => ({
244
327
  r: Math.round(a.r + (b.r - a.r) * t),
245
328
  g: Math.round(a.g + (b.g - a.g) * t),
246
- b: Math.round(a.b + (b.b - a.b) * t),
329
+ b: Math.round(a.b + (b.b - a.b) * t)
247
330
  });
248
331
  function getPaletteColor(palette, position, timeOffset = 0) {
249
332
  if (palette.length === 0) return { r: 255, g: 255, b: 255 };
@@ -263,33 +346,7 @@ function resolvePalette(palette, trailStyle) {
263
346
  }
264
347
  function hexToRgbComponents(hex) {
265
348
  const n = parseInt(hex.slice(1), 16);
266
- return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
267
- }
268
- function computeTangent(trail, i) {
269
- const count = trail.length;
270
- if (count < 2) {
271
- return { x: 1, y: 0 };
272
- }
273
- if (i === 0) {
274
- const dx2 = trail[1].x - trail[0].x;
275
- const dy2 = trail[1].y - trail[0].y;
276
- const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
277
- return { x: dx2 / len2, y: dy2 / len2 };
278
- }
279
- if (i === count - 1) {
280
- const dx2 = trail[count - 1].x - trail[count - 2].x;
281
- const dy2 = trail[count - 1].y - trail[count - 2].y;
282
- const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
283
- return { x: dx2 / len2, y: dy2 / len2 };
284
- }
285
- const dx = trail[i + 1].x - trail[i - 1].x;
286
- const dy = trail[i + 1].y - trail[i - 1].y;
287
- const len = Math.sqrt(dx * dx + dy * dy) || 1;
288
- return { x: dx / len, y: dy / len };
289
- }
290
- function computeNormal(trail, i) {
291
- const tangent = computeTangent(trail, i);
292
- return { x: -tangent.y, y: tangent.x };
349
+ return `${n >> 16},${n >> 8 & 255},${n & 255}`;
293
350
  }
294
351
  function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
295
352
  target.style.width = `${logicalWidth}px`;
@@ -308,7 +365,7 @@ function createRenderer(options) {
308
365
  skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,
309
366
  trailColor: options.trailColor ?? "#ffffff",
310
367
  headColor: options.headColor ?? "#ffffff",
311
- headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
368
+ headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS
312
369
  };
313
370
  const trailStyle = options.trailStyle ?? "default";
314
371
  const palette = resolvePalette(options.palette, trailStyle);
@@ -338,47 +395,8 @@ function createRenderer(options) {
338
395
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
339
396
  let morphAlpha = 0;
340
397
  let gradientAnimTime = 0;
341
- function computeBoundaries(pts) {
342
- if (pts.length === 0) return null;
343
- const first = pts[0];
344
- let minX = first.x,
345
- maxX = first.x,
346
- minY = first.y,
347
- maxY = first.y;
348
- for (const p of pts) {
349
- if (p.x < minX) {
350
- minX = p.x;
351
- }
352
- if (p.x > maxX) {
353
- maxX = p.x;
354
- }
355
- if (p.y < minY) {
356
- minY = p.y;
357
- }
358
- if (p.y > maxY) {
359
- maxY = p.y;
360
- }
361
- }
362
- const width = maxX - minX;
363
- const height = maxY - minY;
364
- if (width === 0 && height === 0) {
365
- throw new Error(
366
- "[sarmal] Degenerate curve: all skeleton points are identical. Check that your curve fn returns distinct points for different values of t.",
367
- );
368
- }
369
- const scaleX = logicalWidth / (width * (1 + FIT_PADDING * 2));
370
- const scaleY = logicalHeight / (height * (1 + FIT_PADDING * 2));
371
- const s = Math.min(scaleX, scaleY);
372
- const boundsWidth = width * s;
373
- const boundsHeight = height * s;
374
- return {
375
- scale: s,
376
- offsetX: (logicalWidth - boundsWidth) / 2 - minX * s,
377
- offsetY: (logicalHeight - boundsHeight) / 2 - minY * s,
378
- };
379
- }
380
398
  function calculateBoundaries() {
381
- const b = computeBoundaries(skeleton);
399
+ const b = computeBoundaries(skeleton, logicalWidth, logicalHeight);
382
400
  if (b) {
383
401
  scale = b.scale;
384
402
  offsetX = b.offsetX;
@@ -442,32 +460,22 @@ function createRenderer(options) {
442
460
  if (trailCount < 2) {
443
461
  return;
444
462
  }
463
+ const toX = (p) => p.x * scale + offsetX;
464
+ const toY = (p) => p.y * scale + offsetY;
445
465
  for (let i = 0; i < trailCount - 1; i++) {
446
- const progress = i / (trailCount - 1);
447
- const nextProgress = (i + 1) / (trailCount - 1);
448
- const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
449
- const width = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
450
- const nextWidth = TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
451
- const curr = trail[i];
452
- const next = trail[i + 1];
453
- const n0 = computeNormal(trail, i);
454
- const n1 = computeNormal(trail, i + 1);
455
- const halfW0 = width / 2;
456
- const halfW1 = nextWidth / 2;
457
- const l0x = curr.x * scale + offsetX + n0.x * halfW0;
458
- const l0y = curr.y * scale + offsetY + n0.y * halfW0;
459
- const r0x = curr.x * scale + offsetX - n0.x * halfW0;
460
- const r0y = curr.y * scale + offsetY - n0.y * halfW0;
461
- const l1x = next.x * scale + offsetX + n1.x * halfW1;
462
- const l1y = next.y * scale + offsetY + n1.y * halfW1;
463
- const r1x = next.x * scale + offsetX - n1.x * halfW1;
464
- const r1y = next.y * scale + offsetY - n1.y * halfW1;
466
+ const { l0x, l0y, r0x, r0y, l1x, l1y, r1x, r1y, opacity, progress } = computeTrailQuad(
467
+ trail,
468
+ i,
469
+ trailCount,
470
+ toX,
471
+ toY
472
+ );
465
473
  if (trailStyle === "default") {
466
- ctx.fillStyle = `rgba(${trailRgb},${alpha})`;
474
+ ctx.fillStyle = `rgba(${trailRgb},${opacity})`;
467
475
  } else {
468
476
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
469
477
  const color = getPaletteColor(palette, progress, timeOffset);
470
- ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${alpha})`;
478
+ ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
471
479
  }
472
480
  ctx.beginPath();
473
481
  ctx.moveTo(l0x, l0y);
@@ -500,7 +508,7 @@ function createRenderer(options) {
500
508
  morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
501
509
  engine.setMorphAlpha(morphAlpha);
502
510
  const interpolatedSkeleton = engine.getSarmalSkeleton();
503
- const bounds = computeBoundaries(interpolatedSkeleton);
511
+ const bounds = computeBoundaries(interpolatedSkeleton, logicalWidth, logicalHeight);
504
512
  if (bounds) {
505
513
  scale = bounds.scale;
506
514
  offsetX = bounds.offsetX;
@@ -561,12 +569,12 @@ function createRenderer(options) {
561
569
  animationId = null;
562
570
  }
563
571
  },
572
+ jump(t, options2) {
573
+ engine.jump(t, options2);
574
+ },
564
575
  seek(t, options2) {
565
576
  engine.seek(t, options2);
566
577
  },
567
- seekWithTrail(t) {
568
- engine.seekWithTrail(t);
569
- },
570
578
  morphTo(target, options2) {
571
579
  if (morphResolve !== null) {
572
580
  engine.completeMorph();
@@ -580,20 +588,33 @@ function createRenderer(options) {
580
588
  return new Promise((resolve) => {
581
589
  morphResolve = resolve;
582
590
  });
583
- },
591
+ }
584
592
  };
585
593
  }
586
594
 
587
595
  // src/renderer-svg.ts
588
- var DEFAULT_MORPH_DURATION_MS2 = 300;
589
596
  var MAX_TRAIL_SEGMENTS = 200;
590
- var TRAIL_FADE_CURVE2 = 1.5;
591
- var TRAIL_MAX_OPACITY2 = 0.88;
592
- var TRAIL_MIN_WIDTH2 = 0.5;
593
- var TRAIL_MAX_WIDTH2 = 2.5;
594
- var DEFAULT_SKELETON_OPACITY2 = 0.15;
595
- var FIT_PADDING2 = 0.1;
596
597
  var EMPTY_PARAMS2 = {};
598
+ function pointsToPathString(pts, scale, offsetX, offsetY) {
599
+ if (pts.length < 2) return "";
600
+ const px = (p) => (p.x * scale + offsetX).toFixed(2);
601
+ const py = (p) => (p.y * scale + offsetY).toFixed(2);
602
+ let d = `M${px(pts[0])} ${py(pts[0])}`;
603
+ for (let i = 1; i < pts.length; i++) {
604
+ d += ` L${px(pts[i])} ${py(pts[i])}`;
605
+ }
606
+ return d + " Z";
607
+ }
608
+ function sampleCurveSkeleton(curveDef) {
609
+ const period = curveDef.period ?? Math.PI * 2;
610
+ const samples = Math.ceil(period * 50);
611
+ const pts = Array.from({ length: samples });
612
+ for (let i = 0; i < samples; i++) {
613
+ const t = i / (samples - 1) * period;
614
+ pts[i] = curveDef.skeletonFn ? curveDef.skeletonFn(t) : curveDef.fn(t, 0, EMPTY_PARAMS2);
615
+ }
616
+ return pts;
617
+ }
597
618
  function el(tag) {
598
619
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
599
620
  }
@@ -604,7 +625,7 @@ function createSVGRenderer(options) {
604
625
  trailColor: options.trailColor ?? "#ffffff",
605
626
  headColor: options.headColor ?? "#ffffff",
606
627
  headRadius: options.headRadius ?? 4,
607
- ariaLabel: options.ariaLabel ?? "Loading",
628
+ ariaLabel: options.ariaLabel ?? "Loading"
608
629
  };
609
630
  const rect = container.getBoundingClientRect();
610
631
  const width = rect.width || 200;
@@ -621,7 +642,7 @@ function createSVGRenderer(options) {
621
642
  const skeletonPath = el("path");
622
643
  skeletonPath.setAttribute("fill", "none");
623
644
  skeletonPath.setAttribute("stroke", opts.skeletonColor);
624
- skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY2));
645
+ skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
625
646
  skeletonPath.setAttribute("stroke-width", "1.5");
626
647
  svg.appendChild(skeletonPath);
627
648
  const skeletonPathA = el("path");
@@ -653,41 +674,13 @@ function createSVGRenderer(options) {
653
674
  let scale = 1;
654
675
  let offsetX = 0;
655
676
  let offsetY = 0;
656
- function calculateBoundaries(skeleton2) {
657
- if (skeleton2.length === 0) {
658
- return;
659
- }
660
- const first = skeleton2[0];
661
- let minX = first.x,
662
- maxX = first.x,
663
- minY = first.y,
664
- maxY = first.y;
665
- for (const p of skeleton2) {
666
- if (p.x < minX) {
667
- minX = p.x;
668
- }
669
- if (p.x > maxX) {
670
- maxX = p.x;
671
- }
672
- if (p.y < minY) {
673
- minY = p.y;
674
- }
675
- if (p.y > maxY) {
676
- maxY = p.y;
677
- }
678
- }
679
- const w = maxX - minX;
680
- const h = maxY - minY;
681
- if (w === 0 && h === 0) {
682
- throw new Error(
683
- "[sarmal] All skeleton points are identical. Check that your curve fn returns distinct points for different values of t.",
684
- );
677
+ function applyBoundaries(skeleton2) {
678
+ const b = computeBoundaries(skeleton2, width, height);
679
+ if (b) {
680
+ scale = b.scale;
681
+ offsetX = b.offsetX;
682
+ offsetY = b.offsetY;
685
683
  }
686
- const scaleX = width / (w * (1 + FIT_PADDING2 * 2));
687
- const scaleY = height / (h * (1 + FIT_PADDING2 * 2));
688
- scale = Math.min(scaleX, scaleY);
689
- offsetX = (width - w * scale) / 2 - minX * scale;
690
- offsetY = (height - h * scale) / 2 - minY * scale;
691
684
  }
692
685
  function px(p) {
693
686
  return p.x * scale + offsetX;
@@ -695,26 +688,11 @@ function createSVGRenderer(options) {
695
688
  function py(p) {
696
689
  return p.y * scale + offsetY;
697
690
  }
698
- function pxStr(p) {
699
- return px(p).toFixed(2);
700
- }
701
- function pyStr(p) {
702
- return py(p).toFixed(2);
703
- }
704
691
  function updateSkeleton(skeleton2) {
705
- if (skeleton2.length < 2) {
706
- skeletonPath.setAttribute("d", "");
707
- return;
708
- }
709
- let d = `M${pxStr(skeleton2[0])} ${pyStr(skeleton2[0])}`;
710
- for (let i = 1; i < skeleton2.length; i++) {
711
- d += ` L${pxStr(skeleton2[i])} ${pyStr(skeleton2[i])}`;
712
- }
713
- d += " Z";
714
- skeletonPath.setAttribute("d", d);
692
+ skeletonPath.setAttribute("d", pointsToPathString(skeleton2, scale, offsetX, offsetY));
715
693
  }
716
694
  const skeleton = engine.getSarmalSkeleton();
717
- calculateBoundaries(skeleton);
695
+ applyBoundaries(skeleton);
718
696
  if (!engine.isLiveSkeleton) {
719
697
  updateSkeleton(skeleton);
720
698
  }
@@ -726,25 +704,13 @@ function createSVGRenderer(options) {
726
704
  return;
727
705
  }
728
706
  for (let i = 0; i < trailCount - 1; i++) {
729
- const progress = i / (trailCount - 1);
730
- const nextProgress = (i + 1) / (trailCount - 1);
731
- const opacity = Math.pow(progress, TRAIL_FADE_CURVE2) * TRAIL_MAX_OPACITY2;
732
- const width2 = TRAIL_MIN_WIDTH2 + progress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
733
- const nextWidth = TRAIL_MIN_WIDTH2 + nextProgress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
734
- const curr = trail[i];
735
- const next = trail[i + 1];
736
- const n0 = computeNormal(trail, i);
737
- const n1 = computeNormal(trail, i + 1);
738
- const halfW0 = width2 / 2;
739
- const halfW1 = nextWidth / 2;
740
- const l0x = px(curr) + n0.x * halfW0;
741
- const l0y = py(curr) + n0.y * halfW0;
742
- const r0x = px(curr) - n0.x * halfW0;
743
- const r0y = py(curr) - n0.y * halfW0;
744
- const l1x = px(next) + n1.x * halfW1;
745
- const l1y = py(next) + n1.y * halfW1;
746
- const r1x = px(next) - n1.x * halfW1;
747
- const r1y = py(next) - n1.y * halfW1;
707
+ const { l0x, l0y, r0x, r0y, l1x, l1y, r1x, r1y, opacity } = computeTrailQuad(
708
+ trail,
709
+ i,
710
+ trailCount,
711
+ px,
712
+ py
713
+ );
748
714
  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`;
749
715
  trailPaths[i].setAttribute("d", d);
750
716
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
@@ -765,33 +731,11 @@ function createSVGRenderer(options) {
765
731
  }
766
732
  let animationId = null;
767
733
  let lastTime = 0;
768
- const prefersReducedMotion =
769
- typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
734
+ const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
770
735
  let morphResolve = null;
771
- let morphDurationMs = DEFAULT_MORPH_DURATION_MS2;
736
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
772
737
  let morphTarget = null;
773
738
  let morphAlpha = 0;
774
- function buildSkeletonPath(target, scale2, offsetX2, offsetY2) {
775
- const period = target.period ?? Math.PI * 2;
776
- const samples = Math.max(50, Math.round(period * 20));
777
- const points = [];
778
- for (let i = 0; i <= samples; i++) {
779
- const t = (i / samples) * period;
780
- const p = target.fn(t, 0, EMPTY_PARAMS2);
781
- points.push(p);
782
- }
783
- if (points.length < 2) {
784
- return "";
785
- }
786
- const px2 = (p) => (p.x * scale2 + offsetX2).toFixed(2);
787
- const py2 = (p) => (p.y * scale2 + offsetY2).toFixed(2);
788
- let d = `M${px2(points[0])} ${py2(points[0])}`;
789
- for (let i = 1; i < points.length; i++) {
790
- d += ` L${px2(points[i])} ${py2(points[i])}`;
791
- }
792
- d += " Z";
793
- return d;
794
- }
795
739
  function renderFrame() {
796
740
  const now = performance.now();
797
741
  const dt = Math.min((now - lastTime) / 1e3, 1 / 30);
@@ -804,16 +748,13 @@ function createSVGRenderer(options) {
804
748
  skeletonPathA.setAttribute("visibility", "visible");
805
749
  skeletonPathA.setAttribute(
806
750
  "stroke-opacity",
807
- String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY2),
751
+ String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY)
808
752
  );
809
753
  }
810
754
  if (morphPathBBuilt) {
811
755
  skeletonPathB.setAttribute("d", morphPathBBuilt);
812
756
  skeletonPathB.setAttribute("visibility", "visible");
813
- skeletonPathB.setAttribute(
814
- "stroke-opacity",
815
- String(morphAlpha * DEFAULT_SKELETON_OPACITY2),
816
- );
757
+ skeletonPathB.setAttribute("stroke-opacity", String(morphAlpha * DEFAULT_SKELETON_OPACITY));
817
758
  }
818
759
  if (morphAlpha >= 1) {
819
760
  engine.completeMorph();
@@ -826,7 +767,7 @@ function createSVGRenderer(options) {
826
767
  skeletonPathA.setAttribute("visibility", "hidden");
827
768
  skeletonPathB.setAttribute("visibility", "hidden");
828
769
  const newSkeleton = engine.getSarmalSkeleton();
829
- calculateBoundaries(newSkeleton);
770
+ applyBoundaries(newSkeleton);
830
771
  updateSkeleton(newSkeleton);
831
772
  }
832
773
  }
@@ -834,7 +775,7 @@ function createSVGRenderer(options) {
834
775
  const trailCount = engine.trailCount;
835
776
  if (engine.isLiveSkeleton && engine.morphAlpha === null) {
836
777
  const liveSkeleton = engine.getSarmalSkeleton();
837
- calculateBoundaries(liveSkeleton);
778
+ applyBoundaries(liveSkeleton);
838
779
  updateSkeleton(liveSkeleton);
839
780
  }
840
781
  updateTrail(trail, trailCount);
@@ -868,12 +809,12 @@ function createSVGRenderer(options) {
868
809
  }
869
810
  svg.remove();
870
811
  },
812
+ jump(t, options2) {
813
+ engine.jump(t, options2);
814
+ },
871
815
  seek(t, options2) {
872
816
  engine.seek(t, options2);
873
817
  },
874
- seekWithTrail(t) {
875
- engine.seekWithTrail(t);
876
- },
877
818
  morphTo(target, options2) {
878
819
  if (morphResolve !== null) {
879
820
  engine.completeMorph();
@@ -883,29 +824,20 @@ function createSVGRenderer(options) {
883
824
  skeletonPathA.setAttribute("visibility", "hidden");
884
825
  skeletonPathB.setAttribute("visibility", "hidden");
885
826
  }
886
- morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS2;
827
+ morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
887
828
  morphTarget = target;
888
829
  morphAlpha = 0;
889
830
  const currentSkeleton = engine.getSarmalSkeleton();
890
- if (currentSkeleton.length >= 2) {
891
- const px2 = (p) => (p.x * scale + offsetX).toFixed(2);
892
- const py2 = (p) => (p.y * scale + offsetY).toFixed(2);
893
- morphPathABuilt = `M${px2(currentSkeleton[0])} ${py2(currentSkeleton[0])}`;
894
- for (let i = 1; i < currentSkeleton.length; i++) {
895
- morphPathABuilt += ` L${px2(currentSkeleton[i])} ${py2(currentSkeleton[i])}`;
896
- }
897
- morphPathABuilt += " Z";
898
- } else {
899
- morphPathABuilt = "";
900
- }
831
+ morphPathABuilt = pointsToPathString(currentSkeleton, scale, offsetX, offsetY);
901
832
  engine.startMorph(target, options2?.morphStrategy);
902
833
  if (morphTarget) {
903
- morphPathBBuilt = buildSkeletonPath(morphTarget, scale, offsetX, offsetY);
834
+ const targetSkeleton = sampleCurveSkeleton(target);
835
+ morphPathBBuilt = pointsToPathString(targetSkeleton, scale, offsetX, offsetY);
904
836
  }
905
837
  return new Promise((resolve) => {
906
838
  morphResolve = resolve;
907
839
  });
908
- },
840
+ }
909
841
  };
910
842
  }
911
843
  function createSarmalSVG(container, curveDef, options) {
@@ -917,22 +849,19 @@ function createSarmalSVG(container, curveDef, options) {
917
849
  // src/curves/artemis2.ts
918
850
  var TWO_PI2 = Math.PI * 2;
919
851
  function artemis2Fn(t, _time, _params) {
920
- const a = 0.35,
921
- b = 0.15,
922
- ox = 0.175;
923
- const s = Math.sin(t),
924
- c = Math.cos(t);
852
+ const a = 0.35, b = 0.15, ox = 0.175;
853
+ const s = Math.sin(t), c = Math.cos(t);
925
854
  const denom = 1 + s * s;
926
855
  return {
927
- x: (c * (1 + a * c)) / denom - ox,
928
- y: (s * c * (1 + b * c)) / denom,
856
+ x: c * (1 + a * c) / denom - ox,
857
+ y: s * c * (1 + b * c) / denom
929
858
  };
930
859
  }
931
860
  var artemis2 = {
932
861
  name: "Artemis II",
933
862
  fn: artemis2Fn,
934
863
  period: TWO_PI2,
935
- speed: 0.7,
864
+ speed: 0.7
936
865
  };
937
866
 
938
867
  // src/curves/astroid.ts
@@ -942,14 +871,14 @@ function astroidFn(t, _time, _params) {
942
871
  const s = Math.sin(t);
943
872
  return {
944
873
  x: c * c * c,
945
- y: s * s * s,
874
+ y: s * s * s
946
875
  };
947
876
  }
948
877
  var astroid = {
949
878
  name: "Astroid",
950
879
  fn: astroidFn,
951
880
  period: TWO_PI3,
952
- speed: 1.1,
881
+ speed: 1.1
953
882
  };
954
883
 
955
884
  // src/curves/deltoid.ts
@@ -957,14 +886,14 @@ var TWO_PI4 = Math.PI * 2;
957
886
  function deltoidFn(t, _time, _params) {
958
887
  return {
959
888
  x: 2 * Math.cos(t) + Math.cos(2 * t),
960
- y: 2 * Math.sin(t) - Math.sin(2 * t),
889
+ y: 2 * Math.sin(t) - Math.sin(2 * t)
961
890
  };
962
891
  }
963
892
  var deltoid = {
964
893
  name: "Deltoid",
965
894
  fn: deltoidFn,
966
895
  period: TWO_PI4,
967
- speed: 0.9,
896
+ speed: 0.9
968
897
  };
969
898
 
970
899
  // src/curves/epicycloid3.ts
@@ -972,14 +901,14 @@ var TWO_PI5 = Math.PI * 2;
972
901
  function epicycloid3Fn(t, _time, _params) {
973
902
  return {
974
903
  x: 4 * Math.cos(t) - Math.cos(4 * t),
975
- y: 4 * Math.sin(t) - Math.sin(4 * t),
904
+ y: 4 * Math.sin(t) - Math.sin(4 * t)
976
905
  };
977
906
  }
978
907
  var epicycloid3 = {
979
908
  name: "Epicycloid (n=3)",
980
909
  fn: epicycloid3Fn,
981
910
  period: TWO_PI5,
982
- speed: 0.75,
911
+ speed: 0.75
983
912
  };
984
913
 
985
914
  // src/curves/epitrochoid7.ts
@@ -988,14 +917,14 @@ function epitrochoid7Fn(t, _time, _params) {
988
917
  const d = 1 + 0.55 * Math.sin(t * 0.5);
989
918
  return {
990
919
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
991
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
920
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
992
921
  };
993
922
  }
994
923
  function epitrochoid7SkeletonFn(t) {
995
924
  const d = 1.275;
996
925
  return {
997
926
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
998
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
927
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
999
928
  };
1000
929
  }
1001
930
  var epitrochoid7 = {
@@ -1003,7 +932,7 @@ var epitrochoid7 = {
1003
932
  fn: epitrochoid7Fn,
1004
933
  period: TWO_PI6,
1005
934
  speed: 1.4,
1006
- skeletonFn: epitrochoid7SkeletonFn,
935
+ skeletonFn: epitrochoid7SkeletonFn
1007
936
  };
1008
937
 
1009
938
  // src/curves/lissajous32.ts
@@ -1012,7 +941,7 @@ function lissajous32Fn(t, time, _params) {
1012
941
  const phi = time * 0.45;
1013
942
  return {
1014
943
  x: Math.sin(3 * t + phi),
1015
- y: Math.sin(2 * t),
944
+ y: Math.sin(2 * t)
1016
945
  };
1017
946
  }
1018
947
  var lissajous32 = {
@@ -1020,7 +949,7 @@ var lissajous32 = {
1020
949
  fn: lissajous32Fn,
1021
950
  period: TWO_PI7,
1022
951
  speed: 2,
1023
- skeleton: "live",
952
+ skeleton: "live"
1024
953
  };
1025
954
 
1026
955
  // src/curves/lissajous43.ts
@@ -1029,7 +958,7 @@ function lissajous43Fn(t, time, _params) {
1029
958
  const phi = time * 0.38;
1030
959
  return {
1031
960
  x: Math.sin(4 * t + phi),
1032
- y: Math.sin(3 * t),
961
+ y: Math.sin(3 * t)
1033
962
  };
1034
963
  }
1035
964
  var lissajous43 = {
@@ -1037,18 +966,17 @@ var lissajous43 = {
1037
966
  fn: lissajous43Fn,
1038
967
  period: TWO_PI8,
1039
968
  speed: 1.8,
1040
- skeleton: "live",
969
+ skeleton: "live"
1041
970
  };
1042
971
 
1043
972
  // src/curves/lame.ts
1044
973
  var TWO_PI9 = Math.PI * 2;
1045
974
  function lameFn(t, time, _params) {
1046
975
  const p = 1.75 + 1.25 * Math.sin(time * 0.48);
1047
- const c = Math.cos(t),
1048
- s = Math.sin(t);
976
+ const c = Math.cos(t), s = Math.sin(t);
1049
977
  return {
1050
978
  x: Math.sign(c) * Math.pow(Math.abs(c), p),
1051
- y: Math.sign(s) * Math.pow(Math.abs(s), p),
979
+ y: Math.sign(s) * Math.pow(Math.abs(s), p)
1052
980
  };
1053
981
  }
1054
982
  var lame = {
@@ -1056,7 +984,7 @@ var lame = {
1056
984
  fn: lameFn,
1057
985
  period: TWO_PI9,
1058
986
  speed: 1,
1059
- skeleton: "live",
987
+ skeleton: "live"
1060
988
  };
1061
989
 
1062
990
  // src/curves/rose3.ts
@@ -1065,14 +993,14 @@ function rose3Fn(t, _time, _params) {
1065
993
  const r = Math.cos(3 * t);
1066
994
  return {
1067
995
  x: r * Math.cos(t),
1068
- y: r * Math.sin(t),
996
+ y: r * Math.sin(t)
1069
997
  };
1070
998
  }
1071
999
  var rose3 = {
1072
1000
  name: "Rose (n=3)",
1073
1001
  fn: rose3Fn,
1074
1002
  period: TWO_PI10,
1075
- speed: 1.15,
1003
+ speed: 1.15
1076
1004
  };
1077
1005
 
1078
1006
  // src/curves/rose5.ts
@@ -1081,14 +1009,14 @@ function rose5Fn(t, _time, _params) {
1081
1009
  const r = Math.cos(5 * t);
1082
1010
  return {
1083
1011
  x: r * Math.cos(t),
1084
- y: r * Math.sin(t),
1012
+ y: r * Math.sin(t)
1085
1013
  };
1086
1014
  }
1087
1015
  var rose5 = {
1088
1016
  name: "Rose (n=5)",
1089
1017
  fn: rose5Fn,
1090
1018
  period: TWO_PI11,
1091
- speed: 1,
1019
+ speed: 1
1092
1020
  };
1093
1021
 
1094
1022
  // src/curves/index.ts
@@ -1102,7 +1030,7 @@ var curves = {
1102
1030
  lissajous32,
1103
1031
  lissajous43,
1104
1032
  epicycloid3,
1105
- lame,
1033
+ lame
1106
1034
  };
1107
1035
 
1108
1036
  // src/index.ts
@@ -1112,23 +1040,6 @@ function createSarmal(canvas, curveDef, options) {
1112
1040
  return createRenderer({ canvas, engine, ...rendererOpts });
1113
1041
  }
1114
1042
 
1115
- export {
1116
- artemis2,
1117
- astroid,
1118
- createEngine,
1119
- createRenderer,
1120
- createSVGRenderer,
1121
- createSarmal,
1122
- createSarmalSVG,
1123
- curves,
1124
- deltoid,
1125
- epicycloid3,
1126
- epitrochoid7,
1127
- lame,
1128
- lissajous32,
1129
- lissajous43,
1130
- rose3,
1131
- rose5,
1132
- };
1133
- //# sourceMappingURL=index.js.map
1043
+ export { artemis2, astroid, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalSVG, curves, deltoid, epicycloid3, epitrochoid7, lame, lissajous32, lissajous43, rose3, rose5 };
1134
1044
  //# sourceMappingURL=index.js.map
1045
+ //# sourceMappingURL=index.js.map