@sarmal/core 0.7.0 → 0.8.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.cjs CHANGED
@@ -1,4 +1,4 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
3
  // src/engine.ts
4
4
  var TWO_PI = Math.PI * 2;
@@ -51,7 +51,7 @@ function resolveCurve(curveDef) {
51
51
  period: curveDef.period ?? TWO_PI,
52
52
  speed: curveDef.speed ?? 1,
53
53
  skeleton: curveDef.skeleton,
54
- skeletonFn: curveDef.skeletonFn
54
+ skeletonFn: curveDef.skeletonFn,
55
55
  };
56
56
  }
57
57
  function createEngine(curveDef, trailLength = 120) {
@@ -77,7 +77,7 @@ function createEngine(curveDef, trailLength = 120) {
77
77
  actualTime += deltaTime;
78
78
  if (morphCurveB !== null && _morphAlpha !== null) {
79
79
  const a = curve.fn(t, actualTime, {});
80
- const tB = _morphStrategy === "normalized" ? t / curve.period * morphCurveB.period : t;
80
+ const tB = _morphStrategy === "normalized" ? (t / curve.period) * morphCurveB.period : t;
81
81
  const b = morphCurveB.fn(tB, actualTime, {});
82
82
  trail.push(a.x + (b.x - a.x) * _morphAlpha, a.y + (b.y - a.y) * _morphAlpha);
83
83
  } else {
@@ -101,14 +101,14 @@ function createEngine(curveDef, trailLength = 120) {
101
101
  trail.clear();
102
102
  },
103
103
  seek(newT, { clearTrail = false } = {}) {
104
- t = (newT % curve.period + curve.period) % curve.period;
104
+ t = ((newT % curve.period) + curve.period) % curve.period;
105
105
  if (clearTrail) {
106
106
  trail.clear();
107
107
  }
108
108
  },
109
109
  seekWithTrail(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
110
110
  const advance = curve.speed * step;
111
- const target = (targetT % curve.period + curve.period) % curve.period;
111
+ const target = ((targetT % curve.period) + curve.period) % curve.period;
112
112
  const targetTime = target / curve.speed;
113
113
  t = target;
114
114
  actualTime = targetTime;
@@ -134,13 +134,16 @@ function createEngine(curveDef, trailLength = 120) {
134
134
  ...frozenB,
135
135
  fn: (sampleT, time, params) => {
136
136
  const a = frozenA.fn(sampleT, time, params);
137
- const tB = frozenStrategy === "normalized" ? sampleT / frozenA.period * frozenB.period : sampleT;
137
+ const tB =
138
+ frozenStrategy === "normalized"
139
+ ? (sampleT / frozenA.period) * frozenB.period
140
+ : sampleT;
138
141
  const b = frozenB.fn(tB, time, params);
139
142
  return {
140
143
  x: a.x + (b.x - a.x) * frozenAlpha,
141
- y: a.y + (b.y - a.y) * frozenAlpha
144
+ y: a.y + (b.y - a.y) * frozenAlpha,
142
145
  };
143
- }
146
+ },
144
147
  };
145
148
  }
146
149
  _morphStrategy = strategy;
@@ -153,7 +156,7 @@ function createEngine(curveDef, trailLength = 120) {
153
156
  completeMorph() {
154
157
  if (morphCurveB !== null) {
155
158
  if (_morphStrategy === "normalized" && curve.period !== morphCurveB.period) {
156
- t = t / curve.period * morphCurveB.period;
159
+ t = (t / curve.period) * morphCurveB.period;
157
160
  }
158
161
  curve = morphCurveB;
159
162
  }
@@ -165,43 +168,74 @@ function createEngine(curveDef, trailLength = 120) {
165
168
  const points = new Array(steps);
166
169
  if (morphCurveB !== null && _morphAlpha !== null) {
167
170
  for (let i = 0; i < steps; i++) {
168
- const sampleT = i / (steps - 1) * curve.period;
171
+ const sampleT = (i / (steps - 1)) * curve.period;
169
172
  const a = sampleSkeleton(curve, sampleT);
170
- const tB = _morphStrategy === "normalized" ? sampleT / curve.period * morphCurveB.period : sampleT;
173
+ const tB =
174
+ _morphStrategy === "normalized"
175
+ ? (sampleT / curve.period) * morphCurveB.period
176
+ : sampleT;
171
177
  const b = sampleSkeleton(morphCurveB, tB);
172
178
  points[i] = {
173
179
  x: a.x + (b.x - a.x) * _morphAlpha,
174
- y: a.y + (b.y - a.y) * _morphAlpha
180
+ y: a.y + (b.y - a.y) * _morphAlpha,
175
181
  };
176
182
  }
177
183
  return points;
178
184
  }
179
185
  for (let i = 0; i < steps; i++) {
180
- const sampleT = i / (steps - 1) * curve.period;
186
+ const sampleT = (i / (steps - 1)) * curve.period;
181
187
  points[i] = sampleSkeleton(curve, sampleT);
182
188
  }
183
189
  return points;
184
- }
190
+ },
185
191
  };
186
192
  }
187
193
 
188
194
  // src/renderer.ts
189
195
  var DEFAULT_MORPH_DURATION_MS = 300;
190
196
  var DEFAULT_HEAD_RADIUS = 4;
191
- var DEFAULT_GLOW_SIZE = 20;
192
197
  var DEFAULT_SKELETON_COLOR = "#ffffff";
193
198
  var DEFAULT_SKELETON_OPACITY = 0.15;
194
199
  var FIT_PADDING = 0.1;
195
- var TRAIL_BATCH_SIZE = 20;
196
200
  var TRAIL_FADE_CURVE = 1.5;
197
201
  var TRAIL_MAX_OPACITY = 0.88;
198
202
  var TRAIL_MIN_WIDTH = 0.5;
199
203
  var TRAIL_MAX_WIDTH = 2.5;
200
- var GLOW_INNER_EDGE = 0.4;
201
- var GLOW_FALLOFF_OPACITY = 0.53;
202
204
  function hexToRgbComponents(hex) {
203
205
  const n = parseInt(hex.slice(1), 16);
204
- return `${n >> 16},${n >> 8 & 255},${n & 255}`;
206
+ return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
207
+ }
208
+ function computeTangent(trail, i) {
209
+ const count = trail.length;
210
+ if (count < 2) {
211
+ return { x: 1, y: 0 };
212
+ }
213
+ if (i === 0) {
214
+ const dx2 = trail[1].x - trail[0].x;
215
+ const dy2 = trail[1].y - trail[0].y;
216
+ const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
217
+ return { x: dx2 / len2, y: dy2 / len2 };
218
+ }
219
+ if (i === count - 1) {
220
+ const dx2 = trail[count - 1].x - trail[count - 2].x;
221
+ const dy2 = trail[count - 1].y - trail[count - 2].y;
222
+ const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
223
+ return { x: dx2 / len2, y: dy2 / len2 };
224
+ }
225
+ const dx = trail[i + 1].x - trail[i - 1].x;
226
+ const dy = trail[i + 1].y - trail[i - 1].y;
227
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
228
+ return { x: dx / len, y: dy / len };
229
+ }
230
+ function computeNormal(trail, i) {
231
+ const tangent = computeTangent(trail, i);
232
+ return { x: -tangent.y, y: tangent.x };
233
+ }
234
+ function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
235
+ target.style.width = `${logicalWidth}px`;
236
+ target.style.height = `${logicalHeight}px`;
237
+ target.width = logicalWidth * dpr;
238
+ target.height = logicalHeight * dpr;
205
239
  }
206
240
  function createRenderer(options) {
207
241
  const canvas = options.canvas;
@@ -215,10 +249,19 @@ function createRenderer(options) {
215
249
  trailColor: options.trailColor ?? "#ffffff",
216
250
  headColor: options.headColor ?? "#ffffff",
217
251
  headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
218
- glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE
219
252
  };
220
253
  const trailRgb = hexToRgbComponents(opts.trailColor);
221
- const headRgbFalloff = `rgba(${hexToRgbComponents(opts.headColor)},${GLOW_FALLOFF_OPACITY})`;
254
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
255
+ function setupCanvas() {
256
+ const rect = canvas.getBoundingClientRect();
257
+ const lw = rect.width || 200;
258
+ const lh = rect.height || 200;
259
+ applyDprSizing(canvas, lw, lh, dpr);
260
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
261
+ }
262
+ setupCanvas();
263
+ let logicalWidth = canvas.width / dpr;
264
+ let logicalHeight = canvas.height / dpr;
222
265
  let skeleton = [];
223
266
  let skeletonCanvas = null;
224
267
  let trail = [];
@@ -232,12 +275,13 @@ function createRenderer(options) {
232
275
  let morphResolve = null;
233
276
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
234
277
  let morphAlpha = 0;
235
- let morphBoundsA = null;
236
- let morphBoundsB = null;
237
278
  function computeBoundaries(pts) {
238
279
  if (pts.length === 0) return null;
239
280
  const first = pts[0];
240
- let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
281
+ let minX = first.x,
282
+ maxX = first.x,
283
+ minY = first.y,
284
+ maxY = first.y;
241
285
  for (const p of pts) {
242
286
  if (p.x < minX) minX = p.x;
243
287
  if (p.x > maxX) maxX = p.x;
@@ -246,17 +290,15 @@ function createRenderer(options) {
246
290
  }
247
291
  const width = maxX - minX;
248
292
  const height = maxY - minY;
249
- const canvasWidth = canvas.width;
250
- const canvasHeight = canvas.height;
251
- const scaleX = canvasWidth / (width * (1 + FIT_PADDING * 2));
252
- const scaleY = canvasHeight / (height * (1 + FIT_PADDING * 2));
293
+ const scaleX = logicalWidth / (width * (1 + FIT_PADDING * 2));
294
+ const scaleY = logicalHeight / (height * (1 + FIT_PADDING * 2));
253
295
  const s = Math.min(scaleX, scaleY);
254
296
  const boundsWidth = width * s;
255
297
  const boundsHeight = height * s;
256
298
  return {
257
299
  scale: s,
258
- offsetX: (canvasWidth - boundsWidth) / 2 - minX * s,
259
- offsetY: (canvasHeight - boundsHeight) / 2 - minY * s
300
+ offsetX: (logicalWidth - boundsWidth) / 2 - minX * s,
301
+ offsetY: (logicalHeight - boundsHeight) / 2 - minY * s,
260
302
  };
261
303
  }
262
304
  function calculateBoundaries() {
@@ -271,6 +313,7 @@ function createRenderer(options) {
271
313
  if (skeleton.length < 2) return;
272
314
  skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
273
315
  const skeletonCtx = skeletonCanvas.getContext("2d");
316
+ skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
274
317
  skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
275
318
  skeletonCtx.lineWidth = 1.5;
276
319
  skeletonCtx.beginPath();
@@ -316,32 +359,41 @@ function createRenderer(options) {
316
359
  }
317
360
  ctx.stroke();
318
361
  } else if (skeletonCanvas) {
319
- ctx.drawImage(skeletonCanvas, 0, 0);
362
+ ctx.drawImage(skeletonCanvas, 0, 0, logicalWidth, logicalHeight);
320
363
  }
321
364
  }
322
365
  function drawTrail() {
323
366
  if (trailCount < 2) {
324
367
  return;
325
368
  }
326
- ctx.lineJoin = "round";
327
- ctx.lineCap = "round";
328
- for (let batchIndex = 0; batchIndex < trailCount - 1; batchIndex += TRAIL_BATCH_SIZE) {
329
- const bEnd = Math.min(batchIndex + TRAIL_BATCH_SIZE, trailCount - 1);
330
- const progress = (batchIndex + bEnd) / 2 / (trailCount - 1);
369
+ for (let i = 0; i < trailCount - 1; i++) {
370
+ const progress = i / (trailCount - 1);
371
+ const nextProgress = (i + 1) / (trailCount - 1);
331
372
  const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
332
- const lineWidth = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
373
+ const width = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
374
+ const nextWidth = TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
375
+ const curr = trail[i];
376
+ const next = trail[i + 1];
377
+ const n0 = computeNormal(trail, i);
378
+ const n1 = computeNormal(trail, i + 1);
379
+ const halfW0 = width / 2;
380
+ const halfW1 = nextWidth / 2;
381
+ const l0x = curr.x * scale + offsetX + n0.x * halfW0;
382
+ const l0y = curr.y * scale + offsetY + n0.y * halfW0;
383
+ const r0x = curr.x * scale + offsetX - n0.x * halfW0;
384
+ const r0y = curr.y * scale + offsetY - n0.y * halfW0;
385
+ const l1x = next.x * scale + offsetX + n1.x * halfW1;
386
+ const l1y = next.y * scale + offsetY + n1.y * halfW1;
387
+ const r1x = next.x * scale + offsetX - n1.x * halfW1;
388
+ const r1y = next.y * scale + offsetY - n1.y * halfW1;
389
+ ctx.fillStyle = `rgba(${trailRgb},${alpha})`;
333
390
  ctx.beginPath();
334
- for (let i = batchIndex; i <= bEnd; i++) {
335
- const point = trail[i];
336
- if (i === batchIndex) {
337
- ctx.moveTo(point.x * scale + offsetX, point.y * scale + offsetY);
338
- } else {
339
- ctx.lineTo(point.x * scale + offsetX, point.y * scale + offsetY);
340
- }
341
- }
342
- ctx.strokeStyle = `rgba(${trailRgb},${alpha})`;
343
- ctx.lineWidth = lineWidth;
344
- ctx.stroke();
391
+ ctx.moveTo(l0x, l0y);
392
+ ctx.lineTo(l1x, l1y);
393
+ ctx.lineTo(r1x, r1y);
394
+ ctx.lineTo(r0x, r0y);
395
+ ctx.closePath();
396
+ ctx.fill();
345
397
  }
346
398
  }
347
399
  function drawHead() {
@@ -350,14 +402,6 @@ function createRenderer(options) {
350
402
  }
351
403
  const x = head.x * scale + offsetX;
352
404
  const y = head.y * scale + offsetY;
353
- const gradient = ctx.createRadialGradient(x, y, 0, x, y, opts.glowSize);
354
- gradient.addColorStop(0, opts.headColor);
355
- gradient.addColorStop(GLOW_INNER_EDGE, headRgbFalloff);
356
- gradient.addColorStop(1, "transparent");
357
- ctx.fillStyle = gradient;
358
- ctx.beginPath();
359
- ctx.arc(x, y, opts.glowSize, 0, Math.PI * 2);
360
- ctx.fill();
361
405
  ctx.fillStyle = opts.headColor;
362
406
  ctx.beginPath();
363
407
  ctx.arc(x, y, opts.headRadius, 0, Math.PI * 2);
@@ -370,20 +414,18 @@ function createRenderer(options) {
370
414
  if (engine.morphAlpha !== null) {
371
415
  morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
372
416
  engine.setMorphAlpha(morphAlpha);
373
- if (morphBoundsA && morphBoundsB) {
374
- const a = morphBoundsA;
375
- const b = morphBoundsB;
376
- scale = a.scale + (b.scale - a.scale) * morphAlpha;
377
- offsetX = a.offsetX + (b.offsetX - a.offsetX) * morphAlpha;
378
- offsetY = a.offsetY + (b.offsetY - a.offsetY) * morphAlpha;
417
+ const interpolatedSkeleton = engine.getSarmalSkeleton();
418
+ const bounds = computeBoundaries(interpolatedSkeleton);
419
+ if (bounds) {
420
+ scale = bounds.scale;
421
+ offsetX = bounds.offsetX;
422
+ offsetY = bounds.offsetY;
379
423
  }
380
424
  if (morphAlpha >= 1) {
381
425
  engine.completeMorph();
382
426
  morphResolve?.();
383
427
  morphResolve = null;
384
428
  morphAlpha = 0;
385
- morphBoundsA = null;
386
- morphBoundsB = null;
387
429
  skeleton = engine.getSarmalSkeleton();
388
430
  if (!engine.isLiveSkeleton) {
389
431
  buildSkeletonCanvas();
@@ -393,7 +435,7 @@ function createRenderer(options) {
393
435
  trail = engine.tick(deltaTime);
394
436
  trailCount = engine.trailCount;
395
437
  head = trailCount > 0 ? trail[trailCount - 1] : null;
396
- ctx.clearRect(0, 0, canvas.width, canvas.height);
438
+ ctx.clearRect(0, 0, logicalWidth, logicalHeight);
397
439
  if (engine.isLiveSkeleton && engine.morphAlpha === null) {
398
440
  skeleton = engine.getSarmalSkeleton();
399
441
  calculateBoundaries();
@@ -441,46 +483,31 @@ function createRenderer(options) {
441
483
  engine.seekWithTrail(t);
442
484
  },
443
485
  morphTo(target, options2) {
444
- const interruptBounds = morphResolve !== null ? { scale, offsetX, offsetY } : null;
445
486
  if (morphResolve !== null) {
446
487
  engine.completeMorph();
447
488
  morphResolve();
448
489
  morphResolve = null;
449
490
  morphAlpha = 0;
450
- morphBoundsA = null;
451
- morphBoundsB = null;
452
491
  }
453
492
  morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
454
493
  morphAlpha = 0;
455
- morphBoundsA = interruptBounds ?? computeBoundaries(engine.getSarmalSkeleton()) ?? { scale, offsetX, offsetY };
456
494
  engine.startMorph(target, options2?.morphStrategy);
457
- const period = target.period ?? Math.PI * 2;
458
- const samples = Math.max(50, Math.round(period * 20));
459
- const skeletonFn = target.skeletonFn ?? ((t) => target.fn(t, 0, {}));
460
- const skeletonB = Array.from(
461
- { length: samples + 1 },
462
- (_, i) => skeletonFn(i / samples * period)
463
- );
464
- morphBoundsB = computeBoundaries(skeletonB) ?? { scale, offsetX, offsetY };
465
495
  return new Promise((resolve) => {
466
496
  morphResolve = resolve;
467
497
  });
468
- }
498
+ },
469
499
  };
470
500
  }
471
501
 
472
502
  // src/renderer-svg.ts
473
503
  var DEFAULT_MORPH_DURATION_MS2 = 300;
474
- var TRAIL_BATCH_COUNT = 12;
504
+ var MAX_TRAIL_SEGMENTS = 200;
475
505
  var TRAIL_FADE_CURVE2 = 1.5;
476
506
  var TRAIL_MAX_OPACITY2 = 0.88;
477
507
  var TRAIL_MIN_WIDTH2 = 0.5;
478
508
  var TRAIL_MAX_WIDTH2 = 2.5;
479
509
  var DEFAULT_SKELETON_OPACITY2 = 0.15;
480
- var DEFAULT_GLOW_INNER_STOP = 0.4;
481
- var DEFAULT_GLOW_FALLOFF_OPACITY = 0.53;
482
510
  var FIT_PADDING2 = 0.1;
483
- var instanceCount = 0;
484
511
  function el(tag) {
485
512
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
486
513
  }
@@ -491,11 +518,8 @@ function createSVGRenderer(options) {
491
518
  trailColor: options.trailColor ?? "#ffffff",
492
519
  headColor: options.headColor ?? "#ffffff",
493
520
  headRadius: options.headRadius ?? 4,
494
- glowSize: options.glowSize ?? 20,
495
- ariaLabel: options.ariaLabel ?? "Loading"
521
+ ariaLabel: options.ariaLabel ?? "Loading",
496
522
  };
497
- const uid = ++instanceCount;
498
- const gradientId = `sarmal-glow-${uid}`;
499
523
  const rect = container.getBoundingClientRect();
500
524
  const width = rect.width || 200;
501
525
  const height = rect.height || 200;
@@ -508,27 +532,6 @@ function createSVGRenderer(options) {
508
532
  const titleEl = el("title");
509
533
  titleEl.textContent = opts.ariaLabel;
510
534
  svg.appendChild(titleEl);
511
- const defs = el("defs");
512
- const gradient = el("radialGradient");
513
- gradient.id = gradientId;
514
- gradient.setAttribute("cx", "50%");
515
- gradient.setAttribute("cy", "50%");
516
- gradient.setAttribute("r", "50%");
517
- const stop0 = el("stop");
518
- stop0.setAttribute("offset", "0%");
519
- stop0.setAttribute("stop-color", opts.headColor);
520
- stop0.setAttribute("stop-opacity", "1");
521
- const stopMid = el("stop");
522
- stopMid.setAttribute("offset", `${DEFAULT_GLOW_INNER_STOP * 100}%`);
523
- stopMid.setAttribute("stop-color", opts.headColor);
524
- stopMid.setAttribute("stop-opacity", String(DEFAULT_GLOW_FALLOFF_OPACITY));
525
- const stop1 = el("stop");
526
- stop1.setAttribute("offset", "100%");
527
- stop1.setAttribute("stop-color", opts.headColor);
528
- stop1.setAttribute("stop-opacity", "0");
529
- gradient.append(stop0, stopMid, stop1);
530
- defs.appendChild(gradient);
531
- svg.appendChild(defs);
532
535
  const skeletonPath = el("path");
533
536
  skeletonPath.setAttribute("fill", "none");
534
537
  skeletonPath.setAttribute("stroke", opts.skeletonColor);
@@ -550,19 +553,12 @@ function createSVGRenderer(options) {
550
553
  let morphPathABuilt = "";
551
554
  let morphPathBBuilt = "";
552
555
  const trailPaths = [];
553
- for (let i = 0; i < TRAIL_BATCH_COUNT; i++) {
556
+ for (let i = 0; i < MAX_TRAIL_SEGMENTS; i++) {
554
557
  const path = el("path");
555
- path.setAttribute("fill", "none");
556
- path.setAttribute("stroke", opts.trailColor);
557
- path.setAttribute("stroke-linecap", "round");
558
- path.setAttribute("stroke-linejoin", "round");
558
+ path.setAttribute("fill", opts.trailColor);
559
559
  svg.appendChild(path);
560
560
  trailPaths.push(path);
561
561
  }
562
- const glowCircle = el("circle");
563
- glowCircle.setAttribute("fill", `url(#${gradientId})`);
564
- glowCircle.setAttribute("r", String(opts.glowSize));
565
- svg.appendChild(glowCircle);
566
562
  const headCircle = el("circle");
567
563
  headCircle.setAttribute("fill", opts.headColor);
568
564
  headCircle.setAttribute("r", String(opts.headRadius));
@@ -576,7 +572,10 @@ function createSVGRenderer(options) {
576
572
  return;
577
573
  }
578
574
  const first = skeleton2[0];
579
- let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
575
+ let minX = first.x,
576
+ maxX = first.x,
577
+ minY = first.y,
578
+ maxY = first.y;
580
579
  for (const p of skeleton2) {
581
580
  if (p.x < minX) {
582
581
  minX = p.x;
@@ -629,24 +628,32 @@ function createSVGRenderer(options) {
629
628
  }
630
629
  return;
631
630
  }
632
- const batchSize = Math.ceil(trailCount / TRAIL_BATCH_COUNT);
633
- for (let b = 0; b < TRAIL_BATCH_COUNT; b++) {
634
- const start = b * batchSize;
635
- const end = Math.min(start + batchSize, trailCount - 1);
636
- if (start >= trailCount - 1) {
637
- trailPaths[b].setAttribute("d", "");
638
- continue;
639
- }
640
- const progress = (start + end) / 2 / (trailCount - 1);
631
+ for (let i = 0; i < trailCount - 1; i++) {
632
+ const progress = i / (trailCount - 1);
633
+ const nextProgress = (i + 1) / (trailCount - 1);
641
634
  const opacity = Math.pow(progress, TRAIL_FADE_CURVE2) * TRAIL_MAX_OPACITY2;
642
- const strokeWidth = TRAIL_MIN_WIDTH2 + progress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
643
- let d = `M${px(trail[start])} ${py(trail[start])}`;
644
- for (let i = start + 1; i <= end; i++) {
645
- d += ` L${px(trail[i])} ${py(trail[i])}`;
646
- }
647
- trailPaths[b].setAttribute("d", d);
648
- trailPaths[b].setAttribute("stroke-opacity", opacity.toFixed(3));
649
- trailPaths[b].setAttribute("stroke-width", strokeWidth.toFixed(2));
635
+ const width2 = TRAIL_MIN_WIDTH2 + progress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
636
+ const nextWidth = TRAIL_MIN_WIDTH2 + nextProgress * (TRAIL_MAX_WIDTH2 - TRAIL_MIN_WIDTH2);
637
+ const curr = trail[i];
638
+ const next = trail[i + 1];
639
+ const n0 = computeNormal(trail, i);
640
+ const n1 = computeNormal(trail, i + 1);
641
+ const halfW0 = width2 / 2;
642
+ const halfW1 = nextWidth / 2;
643
+ const l0x = px(curr) + n0.x * halfW0;
644
+ const l0y = py(curr) + n0.y * halfW0;
645
+ const r0x = px(curr) - n0.x * halfW0;
646
+ const r0y = py(curr) - n0.y * halfW0;
647
+ const l1x = px(next) + n1.x * halfW1;
648
+ const l1y = py(next) + n1.y * halfW1;
649
+ const r1x = px(next) - n1.x * halfW1;
650
+ const r1y = py(next) - n1.y * halfW1;
651
+ 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`;
652
+ trailPaths[i].setAttribute("d", d);
653
+ trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
654
+ }
655
+ for (let i = trailCount - 1; i < trailPaths.length; i++) {
656
+ trailPaths[i].setAttribute("d", "");
650
657
  }
651
658
  }
652
659
  function updateHead(trail, trailCount) {
@@ -656,14 +663,13 @@ function createSVGRenderer(options) {
656
663
  const head = trail[trailCount - 1];
657
664
  const x = px(head);
658
665
  const y = py(head);
659
- glowCircle.setAttribute("cx", x);
660
- glowCircle.setAttribute("cy", y);
661
666
  headCircle.setAttribute("cx", x);
662
667
  headCircle.setAttribute("cy", y);
663
668
  }
664
669
  let animationId = null;
665
670
  let lastTime = 0;
666
- const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
671
+ const prefersReducedMotion =
672
+ typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
667
673
  let morphResolve = null;
668
674
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS2;
669
675
  let morphTarget = null;
@@ -673,7 +679,7 @@ function createSVGRenderer(options) {
673
679
  const samples = Math.max(50, Math.round(period * 20));
674
680
  const points = [];
675
681
  for (let i = 0; i <= samples; i++) {
676
- const t = i / samples * period;
682
+ const t = (i / samples) * period;
677
683
  const p = target.fn(t, 0, {});
678
684
  points.push(p);
679
685
  }
@@ -701,13 +707,16 @@ function createSVGRenderer(options) {
701
707
  skeletonPathA.setAttribute("visibility", "visible");
702
708
  skeletonPathA.setAttribute(
703
709
  "stroke-opacity",
704
- String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY2)
710
+ String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY2),
705
711
  );
706
712
  }
707
713
  if (morphPathBBuilt) {
708
714
  skeletonPathB.setAttribute("d", morphPathBBuilt);
709
715
  skeletonPathB.setAttribute("visibility", "visible");
710
- skeletonPathB.setAttribute("stroke-opacity", String(morphAlpha * DEFAULT_SKELETON_OPACITY2));
716
+ skeletonPathB.setAttribute(
717
+ "stroke-opacity",
718
+ String(morphAlpha * DEFAULT_SKELETON_OPACITY2),
719
+ );
711
720
  }
712
721
  if (morphAlpha >= 1) {
713
722
  engine.completeMorph();
@@ -799,7 +808,7 @@ function createSVGRenderer(options) {
799
808
  return new Promise((resolve) => {
800
809
  morphResolve = resolve;
801
810
  });
802
- }
811
+ },
803
812
  };
804
813
  }
805
814
  function createSarmalSVG(container, curveDef, options) {
@@ -811,26 +820,29 @@ function createSarmalSVG(container, curveDef, options) {
811
820
  // src/curves.ts
812
821
  var TWO_PI2 = Math.PI * 2;
813
822
  function artemis2(t, _time, _params) {
814
- const a = 0.35, b = 0.15, ox = 0.175;
815
- const s = Math.sin(t), c = Math.cos(t);
823
+ const a = 0.35,
824
+ b = 0.15,
825
+ ox = 0.175;
826
+ const s = Math.sin(t),
827
+ c = Math.cos(t);
816
828
  const denom = 1 + s * s;
817
829
  return {
818
- x: c * (1 + a * c) / denom - ox,
819
- y: s * c * (1 + b * c) / denom
830
+ x: (c * (1 + a * c)) / denom - ox,
831
+ y: (s * c * (1 + b * c)) / denom,
820
832
  };
821
833
  }
822
834
  function epitrochoid7(t, _time, _params) {
823
835
  const d = 1 + 0.55 * Math.sin(t * 0.5);
824
836
  return {
825
837
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
826
- y: 7 * Math.sin(t) - d * Math.sin(7 * t)
838
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t),
827
839
  };
828
840
  }
829
841
  function epitrochoid7Skeleton(t) {
830
842
  const d = 1.275;
831
843
  return {
832
844
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
833
- y: 7 * Math.sin(t) - d * Math.sin(7 * t)
845
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t),
834
846
  };
835
847
  }
836
848
  function astroid(t, _time, _params) {
@@ -838,55 +850,56 @@ function astroid(t, _time, _params) {
838
850
  const s = Math.sin(t);
839
851
  return {
840
852
  x: c * c * c,
841
- y: s * s * s
853
+ y: s * s * s,
842
854
  };
843
855
  }
844
856
  function deltoid(t, _time, _params) {
845
857
  return {
846
858
  x: 2 * Math.cos(t) + Math.cos(2 * t),
847
- y: 2 * Math.sin(t) - Math.sin(2 * t)
859
+ y: 2 * Math.sin(t) - Math.sin(2 * t),
848
860
  };
849
861
  }
850
862
  function rose5(t, _time, _params) {
851
863
  const r = Math.cos(5 * t);
852
864
  return {
853
865
  x: r * Math.cos(t),
854
- y: r * Math.sin(t)
866
+ y: r * Math.sin(t),
855
867
  };
856
868
  }
857
869
  function rose3(t, _time, _params) {
858
870
  const r = Math.cos(3 * t);
859
871
  return {
860
872
  x: r * Math.cos(t),
861
- y: r * Math.sin(t)
873
+ y: r * Math.sin(t),
862
874
  };
863
875
  }
864
876
  function lissajous32(t, time, _params) {
865
877
  const phi = time * 0.45;
866
878
  return {
867
879
  x: Math.sin(3 * t + phi),
868
- y: Math.sin(2 * t)
880
+ y: Math.sin(2 * t),
869
881
  };
870
882
  }
871
883
  function lissajous43(t, time, _params) {
872
884
  const phi = time * 0.38;
873
885
  return {
874
886
  x: Math.sin(4 * t + phi),
875
- y: Math.sin(3 * t)
887
+ y: Math.sin(3 * t),
876
888
  };
877
889
  }
878
890
  function epicycloid3(t, _time, _params) {
879
891
  return {
880
892
  x: 4 * Math.cos(t) - Math.cos(4 * t),
881
- y: 4 * Math.sin(t) - Math.sin(4 * t)
893
+ y: 4 * Math.sin(t) - Math.sin(4 * t),
882
894
  };
883
895
  }
884
896
  function lame(t, time, _params) {
885
897
  const p = 1.75 + 1.25 * Math.sin(time * 0.48);
886
- const c = Math.cos(t), s = Math.sin(t);
898
+ const c = Math.cos(t),
899
+ s = Math.sin(t);
887
900
  return {
888
901
  x: Math.sign(c) * Math.pow(Math.abs(c), p),
889
- y: Math.sign(s) * Math.pow(Math.abs(s), p)
902
+ y: Math.sign(s) * Math.pow(Math.abs(s), p),
890
903
  };
891
904
  }
892
905
  var curves = {
@@ -894,66 +907,66 @@ var curves = {
894
907
  name: "Artemis II",
895
908
  fn: artemis2,
896
909
  period: TWO_PI2,
897
- speed: 0.7
910
+ speed: 0.7,
898
911
  },
899
912
  epitrochoid7: {
900
913
  name: "Epitrochoid",
901
914
  fn: epitrochoid7,
902
915
  period: TWO_PI2,
903
916
  speed: 1.4,
904
- skeletonFn: epitrochoid7Skeleton
917
+ skeletonFn: epitrochoid7Skeleton,
905
918
  },
906
919
  astroid: {
907
920
  name: "Astroid",
908
921
  fn: astroid,
909
922
  period: TWO_PI2,
910
- speed: 1.1
923
+ speed: 1.1,
911
924
  },
912
925
  deltoid: {
913
926
  name: "Deltoid",
914
927
  fn: deltoid,
915
928
  period: TWO_PI2,
916
- speed: 0.9
929
+ speed: 0.9,
917
930
  },
918
931
  rose5: {
919
932
  name: "Rose (n=5)",
920
933
  fn: rose5,
921
934
  period: TWO_PI2,
922
- speed: 1
935
+ speed: 1,
923
936
  },
924
937
  rose3: {
925
938
  name: "Rose (n=3)",
926
939
  fn: rose3,
927
940
  period: TWO_PI2,
928
- speed: 1.15
941
+ speed: 1.15,
929
942
  },
930
943
  lissajous32: {
931
944
  name: "Lissajous 3:2",
932
945
  fn: lissajous32,
933
946
  period: TWO_PI2,
934
947
  speed: 2,
935
- skeleton: "live"
948
+ skeleton: "live",
936
949
  },
937
950
  lissajous43: {
938
951
  name: "Lissajous 4:3",
939
952
  fn: lissajous43,
940
953
  period: TWO_PI2,
941
954
  speed: 1.8,
942
- skeleton: "live"
955
+ skeleton: "live",
943
956
  },
944
957
  epicycloid3: {
945
958
  name: "Epicycloid (n=3)",
946
959
  fn: epicycloid3,
947
960
  period: TWO_PI2,
948
- speed: 0.75
961
+ speed: 0.75,
949
962
  },
950
963
  lame: {
951
964
  name: "Lam\xE9 Curve",
952
965
  fn: lame,
953
966
  period: TWO_PI2,
954
967
  speed: 1,
955
- skeleton: "live"
956
- }
968
+ skeleton: "live",
969
+ },
957
970
  };
958
971
 
959
972
  // src/index.ts
@@ -970,4 +983,4 @@ exports.createSarmal = createSarmal;
970
983
  exports.createSarmalSVG = createSarmalSVG;
971
984
  exports.curves = curves;
972
985
  //# sourceMappingURL=index.cjs.map
973
- //# sourceMappingURL=index.cjs.map
986
+ //# sourceMappingURL=index.cjs.map