@sarmal/core 0.9.1 → 0.9.10

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 +196 -157
  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 +195 -156
  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 +233 -265
  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 +233 -282
  56. package/dist/index.js.map +1 -1
  57. package/dist/types-cR2xOewv.d.cts +223 -0
  58. package/dist/types-cR2xOewv.d.ts +223 -0
  59. package/package.json +5 -3
  60. package/dist/types-DX8VfIVK.d.cts +0 -226
  61. package/dist/types-DX8VfIVK.d.ts +0 -226
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // src/engine.ts
2
2
  var TWO_PI = Math.PI * 2;
3
3
  var POINTS_PER_PERIOD_UNIT = 50;
4
+ function lerp(start, end, t) {
5
+ return start + (end - start) * t;
6
+ }
7
+ var EMPTY_PARAMS = {};
4
8
  var CircularBuffer = class {
5
9
  constructor(capacity) {
6
10
  this.head = 0;
@@ -43,16 +47,29 @@ var CircularBuffer = class {
43
47
  }
44
48
  };
45
49
  function resolveCurve(curveDef) {
50
+ const period = curveDef.period ?? TWO_PI;
51
+ if (!Number.isFinite(period) || period <= 0) {
52
+ throw new RangeError(`[sarmal] period must be a positive finite number, got ${period}`);
53
+ }
54
+ const speed = curveDef.speed ?? 1;
55
+ if (!Number.isFinite(speed)) {
56
+ throw new RangeError(`[sarmal] speed must be a finite number, got ${speed}`);
57
+ }
46
58
  return {
47
59
  name: curveDef.name,
48
60
  fn: curveDef.fn,
49
- period: curveDef.period ?? TWO_PI,
50
- speed: curveDef.speed ?? 1,
61
+ period,
62
+ speed,
51
63
  skeleton: curveDef.skeleton,
52
- skeletonFn: curveDef.skeletonFn,
64
+ skeletonFn: curveDef.skeletonFn
53
65
  };
54
66
  }
55
67
  function createEngine(curveDef, trailLength = 120) {
68
+ if (!Number.isFinite(trailLength) || trailLength <= 0) {
69
+ throw new RangeError(
70
+ `[sarmal] trailLength must be a positive finite number, got ${trailLength}`
71
+ );
72
+ }
56
73
  let curve = resolveCurve(curveDef);
57
74
  const trail = new CircularBuffer(trailLength);
58
75
  let t = 0;
@@ -65,21 +82,25 @@ function createEngine(curveDef, trailLength = 120) {
65
82
  return c.skeletonFn(sampleT);
66
83
  }
67
84
  if (c.skeleton === "live") {
68
- return c.fn(sampleT, actualTime, {});
85
+ return c.fn(sampleT, actualTime, EMPTY_PARAMS);
69
86
  }
70
- return c.fn(sampleT, 0, {});
87
+ return c.fn(sampleT, 0, EMPTY_PARAMS);
71
88
  }
72
89
  return {
73
90
  tick(deltaTime) {
74
- t = (t + curve.speed * deltaTime) % curve.period;
91
+ let effectiveSpeed = curve.speed;
92
+ if (morphCurveB !== null && _morphAlpha !== null) {
93
+ effectiveSpeed = lerp(curve.speed, morphCurveB.speed, _morphAlpha);
94
+ }
95
+ t = (t + effectiveSpeed * deltaTime) % curve.period;
75
96
  actualTime += deltaTime;
76
97
  if (morphCurveB !== null && _morphAlpha !== null) {
77
- const a = curve.fn(t, actualTime, {});
78
- const tB = _morphStrategy === "normalized" ? (t / curve.period) * morphCurveB.period : t;
79
- const b = morphCurveB.fn(tB, actualTime, {});
98
+ const a = curve.fn(t, actualTime, EMPTY_PARAMS);
99
+ const tB = _morphStrategy === "normalized" ? t / curve.period * morphCurveB.period : t;
100
+ const b = morphCurveB.fn(tB, actualTime, EMPTY_PARAMS);
80
101
  trail.push(a.x + (b.x - a.x) * _morphAlpha, a.y + (b.y - a.y) * _morphAlpha);
81
102
  } else {
82
- const point = curve.fn(t, actualTime, {});
103
+ const point = curve.fn(t, actualTime, EMPTY_PARAMS);
83
104
  trail.push(point.x, point.y);
84
105
  }
85
106
  return trail.toArray();
@@ -99,14 +120,14 @@ function createEngine(curveDef, trailLength = 120) {
99
120
  trail.clear();
100
121
  },
101
122
  seek(newT, { clearTrail = false } = {}) {
102
- t = ((newT % curve.period) + curve.period) % curve.period;
123
+ t = (newT % curve.period + curve.period) % curve.period;
103
124
  if (clearTrail) {
104
125
  trail.clear();
105
126
  }
106
127
  },
107
128
  seekWithTrail(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
108
129
  const advance = curve.speed * step;
109
- const target = ((targetT % curve.period) + curve.period) % curve.period;
130
+ const target = (targetT % curve.period + curve.period) % curve.period;
110
131
  const targetTime = target / curve.speed;
111
132
  t = target;
112
133
  actualTime = targetTime;
@@ -115,9 +136,9 @@ function createEngine(curveDef, trailLength = 120) {
115
136
  const count = wrap ? trailLength : Math.min(trailLength, pointsFromStart);
116
137
  for (let i = count - 1; i >= 0; i--) {
117
138
  const sampleT = target - i * advance;
118
- const wrappedT = sampleT < 0 ? sampleT + curve.period : sampleT;
139
+ const wrappedT = (sampleT % curve.period + curve.period) % curve.period;
119
140
  const time = targetTime - i * step;
120
- const point = curve.fn(wrappedT, time, {});
141
+ const point = curve.fn(wrappedT, time, EMPTY_PARAMS);
121
142
  trail.push(point.x, point.y);
122
143
  }
123
144
  },
@@ -132,16 +153,13 @@ function createEngine(curveDef, trailLength = 120) {
132
153
  ...frozenB,
133
154
  fn: (sampleT, time, params) => {
134
155
  const a = frozenA.fn(sampleT, time, params);
135
- const tB =
136
- frozenStrategy === "normalized"
137
- ? (sampleT / frozenA.period) * frozenB.period
138
- : sampleT;
156
+ const tB = frozenStrategy === "normalized" ? sampleT / frozenA.period * frozenB.period : sampleT;
139
157
  const b = frozenB.fn(tB, time, params);
140
158
  return {
141
159
  x: a.x + (b.x - a.x) * frozenAlpha,
142
- y: a.y + (b.y - a.y) * frozenAlpha,
160
+ y: a.y + (b.y - a.y) * frozenAlpha
143
161
  };
144
- },
162
+ }
145
163
  };
146
164
  }
147
165
  _morphStrategy = strategy;
@@ -154,7 +172,7 @@ function createEngine(curveDef, trailLength = 120) {
154
172
  completeMorph() {
155
173
  if (morphCurveB !== null) {
156
174
  if (_morphStrategy === "normalized" && curve.period !== morphCurveB.period) {
157
- t = (t / curve.period) * morphCurveB.period;
175
+ t = t / curve.period * morphCurveB.period;
158
176
  }
159
177
  curve = morphCurveB;
160
178
  }
@@ -166,46 +184,132 @@ function createEngine(curveDef, trailLength = 120) {
166
184
  const points = new Array(steps);
167
185
  if (morphCurveB !== null && _morphAlpha !== null) {
168
186
  for (let i = 0; i < steps; i++) {
169
- const sampleT = (i / (steps - 1)) * curve.period;
187
+ const sampleT = i / (steps - 1) * curve.period;
170
188
  const a = sampleSkeleton(curve, sampleT);
171
- const tB =
172
- _morphStrategy === "normalized"
173
- ? (sampleT / curve.period) * morphCurveB.period
174
- : sampleT;
189
+ const tB = _morphStrategy === "normalized" ? sampleT / curve.period * morphCurveB.period : sampleT;
175
190
  const b = sampleSkeleton(morphCurveB, tB);
176
191
  points[i] = {
177
192
  x: a.x + (b.x - a.x) * _morphAlpha,
178
- y: a.y + (b.y - a.y) * _morphAlpha,
193
+ y: a.y + (b.y - a.y) * _morphAlpha
179
194
  };
180
195
  }
181
196
  return points;
182
197
  }
183
198
  for (let i = 0; i < steps; i++) {
184
- const sampleT = (i / (steps - 1)) * curve.period;
199
+ const sampleT = i / (steps - 1) * curve.period;
185
200
  points[i] = sampleSkeleton(curve, sampleT);
186
201
  }
187
202
  return points;
188
- },
203
+ }
189
204
  };
190
205
  }
191
206
 
192
- // src/renderer.ts
207
+ // src/renderer-shared.ts
193
208
  var DEFAULT_MORPH_DURATION_MS = 300;
194
- var DEFAULT_HEAD_RADIUS = 4;
195
- var DEFAULT_SKELETON_COLOR = "#ffffff";
196
209
  var DEFAULT_SKELETON_OPACITY = 0.15;
197
210
  var FIT_PADDING = 0.1;
198
211
  var TRAIL_FADE_CURVE = 1.5;
199
212
  var TRAIL_MAX_OPACITY = 0.88;
200
213
  var TRAIL_MIN_WIDTH = 0.5;
201
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";
202
306
  var GRADIENT = {
203
307
  bard: ["#a855f7", "#3b82f6", "#14b8a6", "#ec4899"],
204
308
  sunset: ["#f97316", "#dc2626", "#9333ea", "#f472b6"],
205
309
  ocean: ["#1e3a8a", "#06b6d4", "#22d3ee", "#e0f2fe"],
206
310
  ice: ["#1e3a8a", "#67e8f9"],
207
311
  fire: ["#7f1d1d", "#fbbf24"],
208
- forest: ["#14532d", "#86efac"],
312
+ forest: ["#14532d", "#86efac"]
209
313
  };
210
314
  var PRESETS = {
211
315
  bard: GRADIENT.bard,
@@ -213,16 +317,16 @@ var PRESETS = {
213
317
  ocean: GRADIENT.ocean,
214
318
  ice: GRADIENT.ice,
215
319
  fire: GRADIENT.fire,
216
- forest: GRADIENT.forest,
320
+ forest: GRADIENT.forest
217
321
  };
218
322
  function hexToRgb(hex) {
219
323
  const n = parseInt(hex.slice(1), 16);
220
- return { r: n >> 16, g: (n >> 8) & 255, b: n & 255 };
324
+ return { r: n >> 16, g: n >> 8 & 255, b: n & 255 };
221
325
  }
222
326
  var lerpRgb = (a, b, t) => ({
223
327
  r: Math.round(a.r + (b.r - a.r) * t),
224
328
  g: Math.round(a.g + (b.g - a.g) * t),
225
- b: Math.round(a.b + (b.b - a.b) * t),
329
+ b: Math.round(a.b + (b.b - a.b) * t)
226
330
  });
227
331
  function getPaletteColor(palette, position, timeOffset = 0) {
228
332
  if (palette.length === 0) return { r: 255, g: 255, b: 255 };
@@ -242,33 +346,7 @@ function resolvePalette(palette, trailStyle) {
242
346
  }
243
347
  function hexToRgbComponents(hex) {
244
348
  const n = parseInt(hex.slice(1), 16);
245
- return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
246
- }
247
- function computeTangent(trail, i) {
248
- const count = trail.length;
249
- if (count < 2) {
250
- return { x: 1, y: 0 };
251
- }
252
- if (i === 0) {
253
- const dx2 = trail[1].x - trail[0].x;
254
- const dy2 = trail[1].y - trail[0].y;
255
- const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
256
- return { x: dx2 / len2, y: dy2 / len2 };
257
- }
258
- if (i === count - 1) {
259
- const dx2 = trail[count - 1].x - trail[count - 2].x;
260
- const dy2 = trail[count - 1].y - trail[count - 2].y;
261
- const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
262
- return { x: dx2 / len2, y: dy2 / len2 };
263
- }
264
- const dx = trail[i + 1].x - trail[i - 1].x;
265
- const dy = trail[i + 1].y - trail[i - 1].y;
266
- const len = Math.sqrt(dx * dx + dy * dy) || 1;
267
- return { x: dx / len, y: dy / len };
268
- }
269
- function computeNormal(trail, i) {
270
- const tangent = computeTangent(trail, i);
271
- return { x: -tangent.y, y: tangent.x };
349
+ return `${n >> 16},${n >> 8 & 255},${n & 255}`;
272
350
  }
273
351
  function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
274
352
  target.style.width = `${logicalWidth}px`;
@@ -287,7 +365,7 @@ function createRenderer(options) {
287
365
  skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,
288
366
  trailColor: options.trailColor ?? "#ffffff",
289
367
  headColor: options.headColor ?? "#ffffff",
290
- headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
368
+ headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS
291
369
  };
292
370
  const trailStyle = options.trailStyle ?? "default";
293
371
  const palette = resolvePalette(options.palette, trailStyle);
@@ -317,34 +395,8 @@ function createRenderer(options) {
317
395
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
318
396
  let morphAlpha = 0;
319
397
  let gradientAnimTime = 0;
320
- function computeBoundaries(pts) {
321
- if (pts.length === 0) return null;
322
- const first = pts[0];
323
- let minX = first.x,
324
- maxX = first.x,
325
- minY = first.y,
326
- maxY = first.y;
327
- for (const p of pts) {
328
- if (p.x < minX) minX = p.x;
329
- if (p.x > maxX) maxX = p.x;
330
- if (p.y < minY) minY = p.y;
331
- if (p.y > maxY) maxY = p.y;
332
- }
333
- const width = maxX - minX;
334
- const height = maxY - minY;
335
- const scaleX = logicalWidth / (width * (1 + FIT_PADDING * 2));
336
- const scaleY = logicalHeight / (height * (1 + FIT_PADDING * 2));
337
- const s = Math.min(scaleX, scaleY);
338
- const boundsWidth = width * s;
339
- const boundsHeight = height * s;
340
- return {
341
- scale: s,
342
- offsetX: (logicalWidth - boundsWidth) / 2 - minX * s,
343
- offsetY: (logicalHeight - boundsHeight) / 2 - minY * s,
344
- };
345
- }
346
398
  function calculateBoundaries() {
347
- const b = computeBoundaries(skeleton);
399
+ const b = computeBoundaries(skeleton, logicalWidth, logicalHeight);
348
400
  if (b) {
349
401
  scale = b.scale;
350
402
  offsetX = b.offsetX;
@@ -408,32 +460,22 @@ function createRenderer(options) {
408
460
  if (trailCount < 2) {
409
461
  return;
410
462
  }
463
+ const toX = (p) => p.x * scale + offsetX;
464
+ const toY = (p) => p.y * scale + offsetY;
411
465
  for (let i = 0; i < trailCount - 1; i++) {
412
- const progress = i / (trailCount - 1);
413
- const nextProgress = (i + 1) / (trailCount - 1);
414
- const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
415
- const width = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
416
- const nextWidth = TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
417
- const curr = trail[i];
418
- const next = trail[i + 1];
419
- const n0 = computeNormal(trail, i);
420
- const n1 = computeNormal(trail, i + 1);
421
- const halfW0 = width / 2;
422
- const halfW1 = nextWidth / 2;
423
- const l0x = curr.x * scale + offsetX + n0.x * halfW0;
424
- const l0y = curr.y * scale + offsetY + n0.y * halfW0;
425
- const r0x = curr.x * scale + offsetX - n0.x * halfW0;
426
- const r0y = curr.y * scale + offsetY - n0.y * halfW0;
427
- const l1x = next.x * scale + offsetX + n1.x * halfW1;
428
- const l1y = next.y * scale + offsetY + n1.y * halfW1;
429
- const r1x = next.x * scale + offsetX - n1.x * halfW1;
430
- 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
+ );
431
473
  if (trailStyle === "default") {
432
- ctx.fillStyle = `rgba(${trailRgb},${alpha})`;
474
+ ctx.fillStyle = `rgba(${trailRgb},${opacity})`;
433
475
  } else {
434
476
  const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
435
477
  const color = getPaletteColor(palette, progress, timeOffset);
436
- ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${alpha})`;
478
+ ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${opacity})`;
437
479
  }
438
480
  ctx.beginPath();
439
481
  ctx.moveTo(l0x, l0y);
@@ -466,7 +508,7 @@ function createRenderer(options) {
466
508
  morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
467
509
  engine.setMorphAlpha(morphAlpha);
468
510
  const interpolatedSkeleton = engine.getSarmalSkeleton();
469
- const bounds = computeBoundaries(interpolatedSkeleton);
511
+ const bounds = computeBoundaries(interpolatedSkeleton, logicalWidth, logicalHeight);
470
512
  if (bounds) {
471
513
  scale = bounds.scale;
472
514
  offsetX = bounds.offsetX;
@@ -546,19 +588,33 @@ function createRenderer(options) {
546
588
  return new Promise((resolve) => {
547
589
  morphResolve = resolve;
548
590
  });
549
- },
591
+ }
550
592
  };
551
593
  }
552
594
 
553
595
  // src/renderer-svg.ts
554
- var DEFAULT_MORPH_DURATION_MS2 = 300;
555
596
  var MAX_TRAIL_SEGMENTS = 200;
556
- var TRAIL_FADE_CURVE2 = 1.5;
557
- var TRAIL_MAX_OPACITY2 = 0.88;
558
- var TRAIL_MIN_WIDTH2 = 0.5;
559
- var TRAIL_MAX_WIDTH2 = 2.5;
560
- var DEFAULT_SKELETON_OPACITY2 = 0.15;
561
- var FIT_PADDING2 = 0.1;
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
+ }
562
618
  function el(tag) {
563
619
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
564
620
  }
@@ -569,7 +625,7 @@ function createSVGRenderer(options) {
569
625
  trailColor: options.trailColor ?? "#ffffff",
570
626
  headColor: options.headColor ?? "#ffffff",
571
627
  headRadius: options.headRadius ?? 4,
572
- ariaLabel: options.ariaLabel ?? "Loading",
628
+ ariaLabel: options.ariaLabel ?? "Loading"
573
629
  };
574
630
  const rect = container.getBoundingClientRect();
575
631
  const width = rect.width || 200;
@@ -586,7 +642,7 @@ function createSVGRenderer(options) {
586
642
  const skeletonPath = el("path");
587
643
  skeletonPath.setAttribute("fill", "none");
588
644
  skeletonPath.setAttribute("stroke", opts.skeletonColor);
589
- skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY2));
645
+ skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
590
646
  skeletonPath.setAttribute("stroke-width", "1.5");
591
647
  svg.appendChild(skeletonPath);
592
648
  const skeletonPathA = el("path");
@@ -618,36 +674,13 @@ function createSVGRenderer(options) {
618
674
  let scale = 1;
619
675
  let offsetX = 0;
620
676
  let offsetY = 0;
621
- function calculateBoundaries(skeleton2) {
622
- if (skeleton2.length === 0) {
623
- return;
624
- }
625
- const first = skeleton2[0];
626
- let minX = first.x,
627
- maxX = first.x,
628
- minY = first.y,
629
- maxY = first.y;
630
- for (const p of skeleton2) {
631
- if (p.x < minX) {
632
- minX = p.x;
633
- }
634
- if (p.x > maxX) {
635
- maxX = p.x;
636
- }
637
- if (p.y < minY) {
638
- minY = p.y;
639
- }
640
- if (p.y > maxY) {
641
- maxY = p.y;
642
- }
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;
643
683
  }
644
- const w = maxX - minX;
645
- const h = maxY - minY;
646
- const scaleX = width / (w * (1 + FIT_PADDING2 * 2));
647
- const scaleY = height / (h * (1 + FIT_PADDING2 * 2));
648
- scale = Math.min(scaleX, scaleY);
649
- offsetX = (width - w * scale) / 2 - minX * scale;
650
- offsetY = (height - h * scale) / 2 - minY * scale;
651
684
  }
652
685
  function px(p) {
653
686
  return p.x * scale + offsetX;
@@ -655,26 +688,11 @@ function createSVGRenderer(options) {
655
688
  function py(p) {
656
689
  return p.y * scale + offsetY;
657
690
  }
658
- function pxStr(p) {
659
- return px(p).toFixed(2);
660
- }
661
- function pyStr(p) {
662
- return py(p).toFixed(2);
663
- }
664
691
  function updateSkeleton(skeleton2) {
665
- if (skeleton2.length < 2) {
666
- skeletonPath.setAttribute("d", "");
667
- return;
668
- }
669
- let d = `M${pxStr(skeleton2[0])} ${pyStr(skeleton2[0])}`;
670
- for (let i = 1; i < skeleton2.length; i++) {
671
- d += ` L${pxStr(skeleton2[i])} ${pyStr(skeleton2[i])}`;
672
- }
673
- d += " Z";
674
- skeletonPath.setAttribute("d", d);
692
+ skeletonPath.setAttribute("d", pointsToPathString(skeleton2, scale, offsetX, offsetY));
675
693
  }
676
694
  const skeleton = engine.getSarmalSkeleton();
677
- calculateBoundaries(skeleton);
695
+ applyBoundaries(skeleton);
678
696
  if (!engine.isLiveSkeleton) {
679
697
  updateSkeleton(skeleton);
680
698
  }
@@ -686,25 +704,13 @@ function createSVGRenderer(options) {
686
704
  return;
687
705
  }
688
706
  for (let i = 0; i < trailCount - 1; i++) {
689
- const progress = i / (trailCount - 1);
690
- const nextProgress = (i + 1) / (trailCount - 1);
691
- const opacity = Math.pow(progress, TRAIL_FADE_CURVE2) * TRAIL_MAX_OPACITY2;
692
- const width2 = TRAIL_MIN_WIDTH2 + progress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
693
- const nextWidth = TRAIL_MIN_WIDTH2 + nextProgress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
694
- const curr = trail[i];
695
- const next = trail[i + 1];
696
- const n0 = computeNormal(trail, i);
697
- const n1 = computeNormal(trail, i + 1);
698
- const halfW0 = width2 / 2;
699
- const halfW1 = nextWidth / 2;
700
- const l0x = px(curr) + n0.x * halfW0;
701
- const l0y = py(curr) + n0.y * halfW0;
702
- const r0x = px(curr) - n0.x * halfW0;
703
- const r0y = py(curr) - n0.y * halfW0;
704
- const l1x = px(next) + n1.x * halfW1;
705
- const l1y = py(next) + n1.y * halfW1;
706
- const r1x = px(next) - n1.x * halfW1;
707
- 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
+ );
708
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`;
709
715
  trailPaths[i].setAttribute("d", d);
710
716
  trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
@@ -725,33 +731,11 @@ function createSVGRenderer(options) {
725
731
  }
726
732
  let animationId = null;
727
733
  let lastTime = 0;
728
- const prefersReducedMotion =
729
- typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
734
+ const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
730
735
  let morphResolve = null;
731
- let morphDurationMs = DEFAULT_MORPH_DURATION_MS2;
736
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
732
737
  let morphTarget = null;
733
738
  let morphAlpha = 0;
734
- function buildSkeletonPath(target, scale2, offsetX2, offsetY2) {
735
- const period = target.period ?? Math.PI * 2;
736
- const samples = Math.max(50, Math.round(period * 20));
737
- const points = [];
738
- for (let i = 0; i <= samples; i++) {
739
- const t = (i / samples) * period;
740
- const p = target.fn(t, 0, {});
741
- points.push(p);
742
- }
743
- if (points.length < 2) {
744
- return "";
745
- }
746
- const px2 = (p) => (p.x * scale2 + offsetX2).toFixed(2);
747
- const py2 = (p) => (p.y * scale2 + offsetY2).toFixed(2);
748
- let d = `M${px2(points[0])} ${py2(points[0])}`;
749
- for (let i = 1; i < points.length; i++) {
750
- d += ` L${px2(points[i])} ${py2(points[i])}`;
751
- }
752
- d += " Z";
753
- return d;
754
- }
755
739
  function renderFrame() {
756
740
  const now = performance.now();
757
741
  const dt = Math.min((now - lastTime) / 1e3, 1 / 30);
@@ -764,16 +748,13 @@ function createSVGRenderer(options) {
764
748
  skeletonPathA.setAttribute("visibility", "visible");
765
749
  skeletonPathA.setAttribute(
766
750
  "stroke-opacity",
767
- String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY2),
751
+ String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY)
768
752
  );
769
753
  }
770
754
  if (morphPathBBuilt) {
771
755
  skeletonPathB.setAttribute("d", morphPathBBuilt);
772
756
  skeletonPathB.setAttribute("visibility", "visible");
773
- skeletonPathB.setAttribute(
774
- "stroke-opacity",
775
- String(morphAlpha * DEFAULT_SKELETON_OPACITY2),
776
- );
757
+ skeletonPathB.setAttribute("stroke-opacity", String(morphAlpha * DEFAULT_SKELETON_OPACITY));
777
758
  }
778
759
  if (morphAlpha >= 1) {
779
760
  engine.completeMorph();
@@ -786,7 +767,7 @@ function createSVGRenderer(options) {
786
767
  skeletonPathA.setAttribute("visibility", "hidden");
787
768
  skeletonPathB.setAttribute("visibility", "hidden");
788
769
  const newSkeleton = engine.getSarmalSkeleton();
789
- calculateBoundaries(newSkeleton);
770
+ applyBoundaries(newSkeleton);
790
771
  updateSkeleton(newSkeleton);
791
772
  }
792
773
  }
@@ -794,7 +775,7 @@ function createSVGRenderer(options) {
794
775
  const trailCount = engine.trailCount;
795
776
  if (engine.isLiveSkeleton && engine.morphAlpha === null) {
796
777
  const liveSkeleton = engine.getSarmalSkeleton();
797
- calculateBoundaries(liveSkeleton);
778
+ applyBoundaries(liveSkeleton);
798
779
  updateSkeleton(liveSkeleton);
799
780
  }
800
781
  updateTrail(trail, trailCount);
@@ -843,29 +824,20 @@ function createSVGRenderer(options) {
843
824
  skeletonPathA.setAttribute("visibility", "hidden");
844
825
  skeletonPathB.setAttribute("visibility", "hidden");
845
826
  }
846
- morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS2;
827
+ morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
847
828
  morphTarget = target;
848
829
  morphAlpha = 0;
849
830
  const currentSkeleton = engine.getSarmalSkeleton();
850
- if (currentSkeleton.length >= 2) {
851
- const px2 = (p) => (p.x * scale + offsetX).toFixed(2);
852
- const py2 = (p) => (p.y * scale + offsetY).toFixed(2);
853
- morphPathABuilt = `M${px2(currentSkeleton[0])} ${py2(currentSkeleton[0])}`;
854
- for (let i = 1; i < currentSkeleton.length; i++) {
855
- morphPathABuilt += ` L${px2(currentSkeleton[i])} ${py2(currentSkeleton[i])}`;
856
- }
857
- morphPathABuilt += " Z";
858
- } else {
859
- morphPathABuilt = "";
860
- }
831
+ morphPathABuilt = pointsToPathString(currentSkeleton, scale, offsetX, offsetY);
861
832
  engine.startMorph(target, options2?.morphStrategy);
862
833
  if (morphTarget) {
863
- morphPathBBuilt = buildSkeletonPath(morphTarget, scale, offsetX, offsetY);
834
+ const targetSkeleton = sampleCurveSkeleton(target);
835
+ morphPathBBuilt = pointsToPathString(targetSkeleton, scale, offsetX, offsetY);
864
836
  }
865
837
  return new Promise((resolve) => {
866
838
  morphResolve = resolve;
867
839
  });
868
- },
840
+ }
869
841
  };
870
842
  }
871
843
  function createSarmalSVG(container, curveDef, options) {
@@ -877,22 +849,19 @@ function createSarmalSVG(container, curveDef, options) {
877
849
  // src/curves/artemis2.ts
878
850
  var TWO_PI2 = Math.PI * 2;
879
851
  function artemis2Fn(t, _time, _params) {
880
- const a = 0.35,
881
- b = 0.15,
882
- ox = 0.175;
883
- const s = Math.sin(t),
884
- 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);
885
854
  const denom = 1 + s * s;
886
855
  return {
887
- x: (c * (1 + a * c)) / denom - ox,
888
- y: (s * c * (1 + b * c)) / denom,
856
+ x: c * (1 + a * c) / denom - ox,
857
+ y: s * c * (1 + b * c) / denom
889
858
  };
890
859
  }
891
860
  var artemis2 = {
892
861
  name: "Artemis II",
893
862
  fn: artemis2Fn,
894
863
  period: TWO_PI2,
895
- speed: 0.7,
864
+ speed: 0.7
896
865
  };
897
866
 
898
867
  // src/curves/astroid.ts
@@ -902,14 +871,14 @@ function astroidFn(t, _time, _params) {
902
871
  const s = Math.sin(t);
903
872
  return {
904
873
  x: c * c * c,
905
- y: s * s * s,
874
+ y: s * s * s
906
875
  };
907
876
  }
908
877
  var astroid = {
909
878
  name: "Astroid",
910
879
  fn: astroidFn,
911
880
  period: TWO_PI3,
912
- speed: 1.1,
881
+ speed: 1.1
913
882
  };
914
883
 
915
884
  // src/curves/deltoid.ts
@@ -917,14 +886,14 @@ var TWO_PI4 = Math.PI * 2;
917
886
  function deltoidFn(t, _time, _params) {
918
887
  return {
919
888
  x: 2 * Math.cos(t) + Math.cos(2 * t),
920
- y: 2 * Math.sin(t) - Math.sin(2 * t),
889
+ y: 2 * Math.sin(t) - Math.sin(2 * t)
921
890
  };
922
891
  }
923
892
  var deltoid = {
924
893
  name: "Deltoid",
925
894
  fn: deltoidFn,
926
895
  period: TWO_PI4,
927
- speed: 0.9,
896
+ speed: 0.9
928
897
  };
929
898
 
930
899
  // src/curves/epicycloid3.ts
@@ -932,14 +901,14 @@ var TWO_PI5 = Math.PI * 2;
932
901
  function epicycloid3Fn(t, _time, _params) {
933
902
  return {
934
903
  x: 4 * Math.cos(t) - Math.cos(4 * t),
935
- y: 4 * Math.sin(t) - Math.sin(4 * t),
904
+ y: 4 * Math.sin(t) - Math.sin(4 * t)
936
905
  };
937
906
  }
938
907
  var epicycloid3 = {
939
908
  name: "Epicycloid (n=3)",
940
909
  fn: epicycloid3Fn,
941
910
  period: TWO_PI5,
942
- speed: 0.75,
911
+ speed: 0.75
943
912
  };
944
913
 
945
914
  // src/curves/epitrochoid7.ts
@@ -948,14 +917,14 @@ function epitrochoid7Fn(t, _time, _params) {
948
917
  const d = 1 + 0.55 * Math.sin(t * 0.5);
949
918
  return {
950
919
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
951
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
920
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
952
921
  };
953
922
  }
954
923
  function epitrochoid7SkeletonFn(t) {
955
924
  const d = 1.275;
956
925
  return {
957
926
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
958
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
927
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
959
928
  };
960
929
  }
961
930
  var epitrochoid7 = {
@@ -963,7 +932,7 @@ var epitrochoid7 = {
963
932
  fn: epitrochoid7Fn,
964
933
  period: TWO_PI6,
965
934
  speed: 1.4,
966
- skeletonFn: epitrochoid7SkeletonFn,
935
+ skeletonFn: epitrochoid7SkeletonFn
967
936
  };
968
937
 
969
938
  // src/curves/lissajous32.ts
@@ -972,7 +941,7 @@ function lissajous32Fn(t, time, _params) {
972
941
  const phi = time * 0.45;
973
942
  return {
974
943
  x: Math.sin(3 * t + phi),
975
- y: Math.sin(2 * t),
944
+ y: Math.sin(2 * t)
976
945
  };
977
946
  }
978
947
  var lissajous32 = {
@@ -980,7 +949,7 @@ var lissajous32 = {
980
949
  fn: lissajous32Fn,
981
950
  period: TWO_PI7,
982
951
  speed: 2,
983
- skeleton: "live",
952
+ skeleton: "live"
984
953
  };
985
954
 
986
955
  // src/curves/lissajous43.ts
@@ -989,7 +958,7 @@ function lissajous43Fn(t, time, _params) {
989
958
  const phi = time * 0.38;
990
959
  return {
991
960
  x: Math.sin(4 * t + phi),
992
- y: Math.sin(3 * t),
961
+ y: Math.sin(3 * t)
993
962
  };
994
963
  }
995
964
  var lissajous43 = {
@@ -997,18 +966,17 @@ var lissajous43 = {
997
966
  fn: lissajous43Fn,
998
967
  period: TWO_PI8,
999
968
  speed: 1.8,
1000
- skeleton: "live",
969
+ skeleton: "live"
1001
970
  };
1002
971
 
1003
972
  // src/curves/lame.ts
1004
973
  var TWO_PI9 = Math.PI * 2;
1005
974
  function lameFn(t, time, _params) {
1006
975
  const p = 1.75 + 1.25 * Math.sin(time * 0.48);
1007
- const c = Math.cos(t),
1008
- s = Math.sin(t);
976
+ const c = Math.cos(t), s = Math.sin(t);
1009
977
  return {
1010
978
  x: Math.sign(c) * Math.pow(Math.abs(c), p),
1011
- y: Math.sign(s) * Math.pow(Math.abs(s), p),
979
+ y: Math.sign(s) * Math.pow(Math.abs(s), p)
1012
980
  };
1013
981
  }
1014
982
  var lame = {
@@ -1016,7 +984,7 @@ var lame = {
1016
984
  fn: lameFn,
1017
985
  period: TWO_PI9,
1018
986
  speed: 1,
1019
- skeleton: "live",
987
+ skeleton: "live"
1020
988
  };
1021
989
 
1022
990
  // src/curves/rose3.ts
@@ -1025,14 +993,14 @@ function rose3Fn(t, _time, _params) {
1025
993
  const r = Math.cos(3 * t);
1026
994
  return {
1027
995
  x: r * Math.cos(t),
1028
- y: r * Math.sin(t),
996
+ y: r * Math.sin(t)
1029
997
  };
1030
998
  }
1031
999
  var rose3 = {
1032
1000
  name: "Rose (n=3)",
1033
1001
  fn: rose3Fn,
1034
1002
  period: TWO_PI10,
1035
- speed: 1.15,
1003
+ speed: 1.15
1036
1004
  };
1037
1005
 
1038
1006
  // src/curves/rose5.ts
@@ -1041,14 +1009,14 @@ function rose5Fn(t, _time, _params) {
1041
1009
  const r = Math.cos(5 * t);
1042
1010
  return {
1043
1011
  x: r * Math.cos(t),
1044
- y: r * Math.sin(t),
1012
+ y: r * Math.sin(t)
1045
1013
  };
1046
1014
  }
1047
1015
  var rose5 = {
1048
1016
  name: "Rose (n=5)",
1049
1017
  fn: rose5Fn,
1050
1018
  period: TWO_PI11,
1051
- speed: 1,
1019
+ speed: 1
1052
1020
  };
1053
1021
 
1054
1022
  // src/curves/index.ts
@@ -1062,7 +1030,7 @@ var curves = {
1062
1030
  lissajous32,
1063
1031
  lissajous43,
1064
1032
  epicycloid3,
1065
- lame,
1033
+ lame
1066
1034
  };
1067
1035
 
1068
1036
  // src/index.ts
@@ -1072,23 +1040,6 @@ function createSarmal(canvas, curveDef, options) {
1072
1040
  return createRenderer({ canvas, engine, ...rendererOpts });
1073
1041
  }
1074
1042
 
1075
- export {
1076
- artemis2,
1077
- astroid,
1078
- createEngine,
1079
- createRenderer,
1080
- createSVGRenderer,
1081
- createSarmal,
1082
- createSarmalSVG,
1083
- curves,
1084
- deltoid,
1085
- epicycloid3,
1086
- epitrochoid7,
1087
- lame,
1088
- lissajous32,
1089
- lissajous43,
1090
- rose3,
1091
- rose5,
1092
- };
1093
- //# sourceMappingURL=index.js.map
1043
+ export { artemis2, astroid, createEngine, createRenderer, createSVGRenderer, createSarmal, createSarmalSVG, curves, deltoid, epicycloid3, epitrochoid7, lame, lissajous32, lissajous43, rose3, rose5 };
1094
1044
  //# sourceMappingURL=index.js.map
1045
+ //# sourceMappingURL=index.js.map