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