@sarmal/core 0.6.0 → 0.7.1

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/auto-init.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,16 +132,13 @@ 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 =
136
- frozenStrategy === "normalized"
137
- ? (sampleT / frozenA.period) * frozenB.period
138
- : sampleT;
135
+ const tB = frozenStrategy === "normalized" ? sampleT / frozenA.period * frozenB.period : sampleT;
139
136
  const b = frozenB.fn(tB, time, params);
140
137
  return {
141
138
  x: a.x + (b.x - a.x) * frozenAlpha,
142
- y: a.y + (b.y - a.y) * frozenAlpha,
139
+ y: a.y + (b.y - a.y) * frozenAlpha
143
140
  };
144
- },
141
+ }
145
142
  };
146
143
  }
147
144
  _morphStrategy = strategy;
@@ -153,6 +150,9 @@ function createEngine(curveDef, trailLength = 120) {
153
150
  },
154
151
  completeMorph() {
155
152
  if (morphCurveB !== null) {
153
+ if (_morphStrategy === "normalized" && curve.period !== morphCurveB.period) {
154
+ t = t / curve.period * morphCurveB.period;
155
+ }
156
156
  curve = morphCurveB;
157
157
  }
158
158
  morphCurveB = null;
@@ -163,26 +163,23 @@ function createEngine(curveDef, trailLength = 120) {
163
163
  const points = new Array(steps);
164
164
  if (morphCurveB !== null && _morphAlpha !== null) {
165
165
  for (let i = 0; i < steps; i++) {
166
- const sampleT = (i / (steps - 1)) * curve.period;
166
+ const sampleT = i / (steps - 1) * curve.period;
167
167
  const a = sampleSkeleton(curve, sampleT);
168
- const tB =
169
- _morphStrategy === "normalized"
170
- ? (sampleT / curve.period) * morphCurveB.period
171
- : sampleT;
168
+ const tB = _morphStrategy === "normalized" ? sampleT / curve.period * morphCurveB.period : sampleT;
172
169
  const b = sampleSkeleton(morphCurveB, tB);
173
170
  points[i] = {
174
171
  x: a.x + (b.x - a.x) * _morphAlpha,
175
- y: a.y + (b.y - a.y) * _morphAlpha,
172
+ y: a.y + (b.y - a.y) * _morphAlpha
176
173
  };
177
174
  }
178
175
  return points;
179
176
  }
180
177
  for (let i = 0; i < steps; i++) {
181
- const sampleT = (i / (steps - 1)) * curve.period;
178
+ const sampleT = i / (steps - 1) * curve.period;
182
179
  points[i] = sampleSkeleton(curve, sampleT);
183
180
  }
184
181
  return points;
185
- },
182
+ }
186
183
  };
187
184
  }
188
185
 
@@ -202,7 +199,7 @@ var GLOW_INNER_EDGE = 0.4;
202
199
  var GLOW_FALLOFF_OPACITY = 0.53;
203
200
  function hexToRgbComponents(hex) {
204
201
  const n = parseInt(hex.slice(1), 16);
205
- return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
202
+ return `${n >> 16},${n >> 8 & 255},${n & 255}`;
206
203
  }
207
204
  function createRenderer(options) {
208
205
  const canvas = options.canvas;
@@ -216,7 +213,7 @@ function createRenderer(options) {
216
213
  trailColor: options.trailColor ?? "#ffffff",
217
214
  headColor: options.headColor ?? "#ffffff",
218
215
  headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
219
- glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE,
216
+ glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE
220
217
  };
221
218
  const trailRgb = hexToRgbComponents(opts.trailColor);
222
219
  const headRgbFalloff = `rgba(${hexToRgbComponents(opts.headColor)},${GLOW_FALLOFF_OPACITY})`;
@@ -232,32 +229,16 @@ function createRenderer(options) {
232
229
  let lastTime = 0;
233
230
  let morphResolve = null;
234
231
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
235
- let morphTarget = null;
236
232
  let morphAlpha = 0;
237
- let skeletonCanvasA = null;
238
- let skeletonCanvasB = null;
239
- function calculateBoundaries() {
240
- if (skeleton.length === 0) {
241
- return;
242
- }
243
- const first = skeleton[0];
244
- let minX = first.x,
245
- maxX = first.x,
246
- minY = first.y,
247
- maxY = first.y;
248
- for (const p of skeleton) {
249
- if (p.x < minX) {
250
- minX = p.x;
251
- }
252
- if (p.x > maxX) {
253
- maxX = p.x;
254
- }
255
- if (p.y < minY) {
256
- minY = p.y;
257
- }
258
- if (p.y > maxY) {
259
- maxY = p.y;
260
- }
233
+ function computeBoundaries(pts) {
234
+ if (pts.length === 0) return null;
235
+ const first = pts[0];
236
+ let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
237
+ for (const p of pts) {
238
+ if (p.x < minX) minX = p.x;
239
+ if (p.x > maxX) maxX = p.x;
240
+ if (p.y < minY) minY = p.y;
241
+ if (p.y > maxY) maxY = p.y;
261
242
  }
262
243
  const width = maxX - minX;
263
244
  const height = maxY - minY;
@@ -265,11 +246,22 @@ function createRenderer(options) {
265
246
  const canvasHeight = canvas.height;
266
247
  const scaleX = canvasWidth / (width * (1 + FIT_PADDING * 2));
267
248
  const scaleY = canvasHeight / (height * (1 + FIT_PADDING * 2));
268
- scale = Math.min(scaleX, scaleY);
269
- const boundsWidth = width * scale;
270
- const boundsHeight = height * scale;
271
- offsetX = (canvasWidth - boundsWidth) / 2 - minX * scale;
272
- offsetY = (canvasHeight - boundsHeight) / 2 - minY * scale;
249
+ const s = Math.min(scaleX, scaleY);
250
+ const boundsWidth = width * s;
251
+ const boundsHeight = height * s;
252
+ return {
253
+ scale: s,
254
+ offsetX: (canvasWidth - boundsWidth) / 2 - minX * s,
255
+ offsetY: (canvasHeight - boundsHeight) / 2 - minY * s
256
+ };
257
+ }
258
+ function calculateBoundaries() {
259
+ const b = computeBoundaries(skeleton);
260
+ if (b) {
261
+ scale = b.scale;
262
+ offsetX = b.offsetX;
263
+ offsetY = b.offsetY;
264
+ }
273
265
  }
274
266
  function buildSkeletonCanvas() {
275
267
  if (skeleton.length < 2) return;
@@ -286,20 +278,23 @@ function createRenderer(options) {
286
278
  }
287
279
  skeletonCtx.stroke();
288
280
  }
281
+ function drawSkeletonPath(pts, opacity) {
282
+ if (pts.length < 2) return;
283
+ ctx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${opacity})`;
284
+ ctx.lineWidth = 1.5;
285
+ ctx.beginPath();
286
+ ctx.moveTo(pts[0].x * scale + offsetX, pts[0].y * scale + offsetY);
287
+ for (let i = 1; i < pts.length; i++) {
288
+ ctx.lineTo(pts[i].x * scale + offsetX, pts[i].y * scale + offsetY);
289
+ }
290
+ ctx.stroke();
291
+ }
289
292
  function drawSkeleton() {
290
293
  if (opts.skeletonColor === "transparent") {
291
294
  return;
292
295
  }
293
296
  if (engine.morphAlpha !== null) {
294
- if (skeletonCanvasA) {
295
- ctx.globalAlpha = (1 - morphAlpha) * DEFAULT_SKELETON_OPACITY;
296
- ctx.drawImage(skeletonCanvasA, 0, 0);
297
- }
298
- if (skeletonCanvasB) {
299
- ctx.globalAlpha = morphAlpha * DEFAULT_SKELETON_OPACITY;
300
- ctx.drawImage(skeletonCanvasB, 0, 0);
301
- }
302
- ctx.globalAlpha = 1;
297
+ drawSkeletonPath(engine.getSarmalSkeleton(), DEFAULT_SKELETON_OPACITY);
303
298
  return;
304
299
  }
305
300
  if (engine.isLiveSkeleton) {
@@ -371,23 +366,29 @@ function createRenderer(options) {
371
366
  if (engine.morphAlpha !== null) {
372
367
  morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
373
368
  engine.setMorphAlpha(morphAlpha);
374
- skeleton = engine.getSarmalSkeleton();
375
- calculateBoundaries();
369
+ const interpolatedSkeleton = engine.getSarmalSkeleton();
370
+ const bounds = computeBoundaries(interpolatedSkeleton);
371
+ if (bounds) {
372
+ scale = bounds.scale;
373
+ offsetX = bounds.offsetX;
374
+ offsetY = bounds.offsetY;
375
+ }
376
376
  if (morphAlpha >= 1) {
377
377
  engine.completeMorph();
378
378
  morphResolve?.();
379
379
  morphResolve = null;
380
- morphTarget = null;
381
380
  morphAlpha = 0;
382
- skeletonCanvasA = null;
383
- skeletonCanvasB = null;
381
+ skeleton = engine.getSarmalSkeleton();
382
+ if (!engine.isLiveSkeleton) {
383
+ buildSkeletonCanvas();
384
+ }
384
385
  }
385
386
  }
386
387
  trail = engine.tick(deltaTime);
387
388
  trailCount = engine.trailCount;
388
389
  head = trailCount > 0 ? trail[trailCount - 1] : null;
389
390
  ctx.clearRect(0, 0, canvas.width, canvas.height);
390
- if (engine.isLiveSkeleton || engine.morphAlpha !== null) {
391
+ if (engine.isLiveSkeleton && engine.morphAlpha === null) {
391
392
  skeleton = engine.getSarmalSkeleton();
392
393
  calculateBoundaries();
393
394
  }
@@ -439,78 +440,40 @@ function createRenderer(options) {
439
440
  morphResolve();
440
441
  morphResolve = null;
441
442
  morphAlpha = 0;
442
- skeletonCanvasA = null;
443
- skeletonCanvasB = null;
444
443
  }
445
444
  morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
446
- morphTarget = target;
447
445
  morphAlpha = 0;
448
- const currentSkeleton = engine.getSarmalSkeleton();
449
- if (currentSkeleton.length >= 2) {
450
- skeletonCanvasA = new OffscreenCanvas(canvas.width, canvas.height);
451
- const ctxA = skeletonCanvasA.getContext("2d");
452
- ctxA.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
453
- ctxA.lineWidth = 1.5;
454
- ctxA.beginPath();
455
- const first = currentSkeleton[0];
456
- ctxA.moveTo(first.x * scale + offsetX, first.y * scale + offsetY);
457
- for (let i = 1; i < currentSkeleton.length; i++) {
458
- const p = currentSkeleton[i];
459
- ctxA.lineTo(p.x * scale + offsetX, p.y * scale + offsetY);
460
- }
461
- ctxA.stroke();
462
- }
463
446
  engine.startMorph(target, options2?.morphStrategy);
464
- if (morphTarget && !engine.isLiveSkeleton) {
465
- skeletonCanvasB = new OffscreenCanvas(canvas.width, canvas.height);
466
- const skeletonCtx = skeletonCanvasB.getContext("2d");
467
- skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
468
- skeletonCtx.lineWidth = 1.5;
469
- skeletonCtx.beginPath();
470
- const period = morphTarget.period ?? Math.PI * 2;
471
- const samples = Math.max(50, Math.round(period * 20));
472
- const firstB = morphTarget.fn(0, 0, {});
473
- skeletonCtx.moveTo(firstB.x * scale + offsetX, firstB.y * scale + offsetY);
474
- for (let i = 1; i <= samples; i++) {
475
- const t = (i / samples) * period;
476
- const p = morphTarget.fn(t, 0, {});
477
- skeletonCtx.lineTo(p.x * scale + offsetX, p.y * scale + offsetY);
478
- }
479
- skeletonCtx.stroke();
480
- }
481
447
  return new Promise((resolve) => {
482
448
  morphResolve = resolve;
483
449
  });
484
- },
450
+ }
485
451
  };
486
452
  }
487
453
 
488
454
  // src/curves.ts
489
455
  var TWO_PI2 = Math.PI * 2;
490
456
  function artemis2(t, _time, _params) {
491
- const a = 0.35,
492
- b = 0.15,
493
- ox = 0.175;
494
- const s = Math.sin(t),
495
- c = Math.cos(t);
457
+ const a = 0.35, b = 0.15, ox = 0.175;
458
+ const s = Math.sin(t), c = Math.cos(t);
496
459
  const denom = 1 + s * s;
497
460
  return {
498
- x: (c * (1 + a * c)) / denom - ox,
499
- y: (s * c * (1 + b * c)) / denom,
461
+ x: c * (1 + a * c) / denom - ox,
462
+ y: s * c * (1 + b * c) / denom
500
463
  };
501
464
  }
502
465
  function epitrochoid7(t, _time, _params) {
503
466
  const d = 1 + 0.55 * Math.sin(t * 0.5);
504
467
  return {
505
468
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
506
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
469
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
507
470
  };
508
471
  }
509
472
  function epitrochoid7Skeleton(t) {
510
473
  const d = 1.275;
511
474
  return {
512
475
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
513
- y: 7 * Math.sin(t) - d * Math.sin(7 * t),
476
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
514
477
  };
515
478
  }
516
479
  function astroid(t, _time, _params) {
@@ -518,56 +481,55 @@ function astroid(t, _time, _params) {
518
481
  const s = Math.sin(t);
519
482
  return {
520
483
  x: c * c * c,
521
- y: s * s * s,
484
+ y: s * s * s
522
485
  };
523
486
  }
524
487
  function deltoid(t, _time, _params) {
525
488
  return {
526
489
  x: 2 * Math.cos(t) + Math.cos(2 * t),
527
- y: 2 * Math.sin(t) - Math.sin(2 * t),
490
+ y: 2 * Math.sin(t) - Math.sin(2 * t)
528
491
  };
529
492
  }
530
493
  function rose5(t, _time, _params) {
531
494
  const r = Math.cos(5 * t);
532
495
  return {
533
496
  x: r * Math.cos(t),
534
- y: r * Math.sin(t),
497
+ y: r * Math.sin(t)
535
498
  };
536
499
  }
537
500
  function rose3(t, _time, _params) {
538
501
  const r = Math.cos(3 * t);
539
502
  return {
540
503
  x: r * Math.cos(t),
541
- y: r * Math.sin(t),
504
+ y: r * Math.sin(t)
542
505
  };
543
506
  }
544
507
  function lissajous32(t, time, _params) {
545
508
  const phi = time * 0.45;
546
509
  return {
547
510
  x: Math.sin(3 * t + phi),
548
- y: Math.sin(2 * t),
511
+ y: Math.sin(2 * t)
549
512
  };
550
513
  }
551
514
  function lissajous43(t, time, _params) {
552
515
  const phi = time * 0.38;
553
516
  return {
554
517
  x: Math.sin(4 * t + phi),
555
- y: Math.sin(3 * t),
518
+ y: Math.sin(3 * t)
556
519
  };
557
520
  }
558
521
  function epicycloid3(t, _time, _params) {
559
522
  return {
560
523
  x: 4 * Math.cos(t) - Math.cos(4 * t),
561
- y: 4 * Math.sin(t) - Math.sin(4 * t),
524
+ y: 4 * Math.sin(t) - Math.sin(4 * t)
562
525
  };
563
526
  }
564
527
  function lame(t, time, _params) {
565
528
  const p = 1.75 + 1.25 * Math.sin(time * 0.48);
566
- const c = Math.cos(t),
567
- s = Math.sin(t);
529
+ const c = Math.cos(t), s = Math.sin(t);
568
530
  return {
569
531
  x: Math.sign(c) * Math.pow(Math.abs(c), p),
570
- y: Math.sign(s) * Math.pow(Math.abs(s), p),
532
+ y: Math.sign(s) * Math.pow(Math.abs(s), p)
571
533
  };
572
534
  }
573
535
  var curves = {
@@ -575,66 +537,66 @@ var curves = {
575
537
  name: "Artemis II",
576
538
  fn: artemis2,
577
539
  period: TWO_PI2,
578
- speed: 0.7,
540
+ speed: 0.7
579
541
  },
580
542
  epitrochoid7: {
581
543
  name: "Epitrochoid",
582
544
  fn: epitrochoid7,
583
545
  period: TWO_PI2,
584
546
  speed: 1.4,
585
- skeletonFn: epitrochoid7Skeleton,
547
+ skeletonFn: epitrochoid7Skeleton
586
548
  },
587
549
  astroid: {
588
550
  name: "Astroid",
589
551
  fn: astroid,
590
552
  period: TWO_PI2,
591
- speed: 1.1,
553
+ speed: 1.1
592
554
  },
593
555
  deltoid: {
594
556
  name: "Deltoid",
595
557
  fn: deltoid,
596
558
  period: TWO_PI2,
597
- speed: 0.9,
559
+ speed: 0.9
598
560
  },
599
561
  rose5: {
600
562
  name: "Rose (n=5)",
601
563
  fn: rose5,
602
564
  period: TWO_PI2,
603
- speed: 1,
565
+ speed: 1
604
566
  },
605
567
  rose3: {
606
568
  name: "Rose (n=3)",
607
569
  fn: rose3,
608
570
  period: TWO_PI2,
609
- speed: 1.15,
571
+ speed: 1.15
610
572
  },
611
573
  lissajous32: {
612
574
  name: "Lissajous 3:2",
613
575
  fn: lissajous32,
614
576
  period: TWO_PI2,
615
577
  speed: 2,
616
- skeleton: "live",
578
+ skeleton: "live"
617
579
  },
618
580
  lissajous43: {
619
581
  name: "Lissajous 4:3",
620
582
  fn: lissajous43,
621
583
  period: TWO_PI2,
622
584
  speed: 1.8,
623
- skeleton: "live",
585
+ skeleton: "live"
624
586
  },
625
587
  epicycloid3: {
626
588
  name: "Epicycloid (n=3)",
627
589
  fn: epicycloid3,
628
590
  period: TWO_PI2,
629
- speed: 0.75,
591
+ speed: 0.75
630
592
  },
631
593
  lame: {
632
594
  name: "Lam\xE9 Curve",
633
595
  fn: lame,
634
596
  period: TWO_PI2,
635
597
  speed: 1,
636
- skeleton: "live",
637
- },
598
+ skeleton: "live"
599
+ }
638
600
  };
639
601
 
640
602
  // src/index.ts
@@ -657,12 +619,12 @@ function init() {
657
619
  return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
658
620
  }
659
621
  const sarmal = createSarmal(canvas, curveDef, {
660
- ...(canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor }),
661
- ...(canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor }),
662
- ...(canvas.dataset.headColor && { headColor: canvas.dataset.headColor }),
663
- ...(canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) }),
664
- ...(canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) }),
665
- ...(canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }),
622
+ ...canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor },
623
+ ...canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor },
624
+ ...canvas.dataset.headColor && { headColor: canvas.dataset.headColor },
625
+ ...canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) },
626
+ ...canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) },
627
+ ...canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }
666
628
  });
667
629
  sarmal.start();
668
630
  });
@@ -673,4 +635,4 @@ if (document.readyState === "loading") {
673
635
  init();
674
636
  }
675
637
  //# sourceMappingURL=auto-init.js.map
676
- //# sourceMappingURL=auto-init.js.map
638
+ //# sourceMappingURL=auto-init.js.map