@sarmal/core 0.5.0 → 0.6.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.
@@ -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;
@@ -44,24 +44,46 @@ var CircularBuffer = class {
44
44
  return this.count;
45
45
  }
46
46
  };
47
- function createEngine(curveDef, trailLength = 120) {
48
- const curve = {
47
+ function resolveCurve(curveDef) {
48
+ return {
49
49
  name: curveDef.name,
50
50
  fn: curveDef.fn,
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
+ }
57
+ function createEngine(curveDef, trailLength = 120) {
58
+ let curve = resolveCurve(curveDef);
56
59
  const trail = new CircularBuffer(trailLength);
57
60
  let t = 0;
58
61
  let actualTime = 0;
62
+ let morphCurveB = null;
63
+ let _morphAlpha = null;
64
+ let _morphStrategy = "normalized";
65
+ function sampleSkeleton(c, sampleT) {
66
+ if (c.skeletonFn) {
67
+ return c.skeletonFn(sampleT);
68
+ }
69
+ if (c.skeleton === "live") {
70
+ return c.fn(sampleT, actualTime, {});
71
+ }
72
+ return c.fn(sampleT, 0, {});
73
+ }
59
74
  return {
60
75
  tick(deltaTime) {
61
76
  t = (t + curve.speed * deltaTime) % curve.period;
62
77
  actualTime += deltaTime;
63
- const point = curve.fn(t, actualTime, {});
64
- trail.push(point.x, point.y);
78
+ if (morphCurveB !== null && _morphAlpha !== null) {
79
+ const a = curve.fn(t, actualTime, {});
80
+ const tB = _morphStrategy === "normalized" ? (t / curve.period) * morphCurveB.period : t;
81
+ const b = morphCurveB.fn(tB, actualTime, {});
82
+ trail.push(a.x + (b.x - a.x) * _morphAlpha, a.y + (b.y - a.y) * _morphAlpha);
83
+ } else {
84
+ const point = curve.fn(t, actualTime, {});
85
+ trail.push(point.x, point.y);
86
+ }
65
87
  return trail.toArray();
66
88
  },
67
89
  get trailCount() {
@@ -70,20 +92,23 @@ function createEngine(curveDef, trailLength = 120) {
70
92
  get isLiveSkeleton() {
71
93
  return curve.skeleton === "live";
72
94
  },
95
+ get morphAlpha() {
96
+ return _morphAlpha;
97
+ },
73
98
  reset() {
74
99
  t = 0;
75
100
  actualTime = 0;
76
101
  trail.clear();
77
102
  },
78
103
  seek(newT, { clearTrail = false } = {}) {
79
- t = (newT % curve.period + curve.period) % curve.period;
104
+ t = ((newT % curve.period) + curve.period) % curve.period;
80
105
  if (clearTrail) {
81
106
  trail.clear();
82
107
  }
83
108
  },
84
109
  seekWithTrail(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
85
110
  const advance = curve.speed * step;
86
- const target = (targetT % curve.period + curve.period) % curve.period;
111
+ const target = ((targetT % curve.period) + curve.period) % curve.period;
87
112
  const targetTime = target / curve.speed;
88
113
  t = target;
89
114
  actualTime = targetTime;
@@ -98,31 +123,73 @@ function createEngine(curveDef, trailLength = 120) {
98
123
  trail.push(point.x, point.y);
99
124
  }
100
125
  },
126
+ startMorph(target, strategy = "normalized") {
127
+ const resolvedTarget = resolveCurve(target);
128
+ if (morphCurveB !== null && _morphAlpha !== null) {
129
+ const frozenAlpha = _morphAlpha;
130
+ const frozenA = curve;
131
+ const frozenB = morphCurveB;
132
+ const frozenStrategy = _morphStrategy;
133
+ curve = {
134
+ ...frozenB,
135
+ fn: (sampleT, time, params) => {
136
+ const a = frozenA.fn(sampleT, time, params);
137
+ const tB =
138
+ frozenStrategy === "normalized"
139
+ ? (sampleT / frozenA.period) * frozenB.period
140
+ : sampleT;
141
+ const b = frozenB.fn(tB, time, params);
142
+ return {
143
+ x: a.x + (b.x - a.x) * frozenAlpha,
144
+ y: a.y + (b.y - a.y) * frozenAlpha,
145
+ };
146
+ },
147
+ };
148
+ }
149
+ _morphStrategy = strategy;
150
+ morphCurveB = resolvedTarget;
151
+ _morphAlpha = 0;
152
+ },
153
+ setMorphAlpha(alpha) {
154
+ _morphAlpha = alpha;
155
+ },
156
+ completeMorph() {
157
+ if (morphCurveB !== null) {
158
+ curve = morphCurveB;
159
+ }
160
+ morphCurveB = null;
161
+ _morphAlpha = null;
162
+ },
101
163
  getSarmalSkeleton() {
102
164
  const steps = Math.ceil(curve.period * POINTS_PER_PERIOD_UNIT);
103
165
  const points = new Array(steps);
104
- if (curve.skeletonFn) {
105
- for (let i = 0; i < steps; i++) {
106
- const sampleT = i / (steps - 1) * curve.period;
107
- points[i] = curve.skeletonFn(sampleT);
108
- }
109
- } else if (curve.skeleton === "live") {
166
+ if (morphCurveB !== null && _morphAlpha !== null) {
110
167
  for (let i = 0; i < steps; i++) {
111
- const sampleT = i / (steps - 1) * curve.period;
112
- points[i] = curve.fn(sampleT, actualTime, {});
113
- }
114
- } else {
115
- for (let i = 0; i < steps; i++) {
116
- const sampleT = i / (steps - 1) * curve.period;
117
- points[i] = curve.fn(sampleT, 0, {});
168
+ const sampleT = (i / (steps - 1)) * curve.period;
169
+ const a = sampleSkeleton(curve, sampleT);
170
+ const tB =
171
+ _morphStrategy === "normalized"
172
+ ? (sampleT / curve.period) * morphCurveB.period
173
+ : sampleT;
174
+ const b = sampleSkeleton(morphCurveB, tB);
175
+ points[i] = {
176
+ x: a.x + (b.x - a.x) * _morphAlpha,
177
+ y: a.y + (b.y - a.y) * _morphAlpha,
178
+ };
118
179
  }
180
+ return points;
181
+ }
182
+ for (let i = 0; i < steps; i++) {
183
+ const sampleT = (i / (steps - 1)) * curve.period;
184
+ points[i] = sampleSkeleton(curve, sampleT);
119
185
  }
120
186
  return points;
121
- }
187
+ },
122
188
  };
123
189
  }
124
190
 
125
191
  // src/renderer.ts
192
+ var DEFAULT_MORPH_DURATION_MS = 300;
126
193
  var DEFAULT_HEAD_RADIUS = 4;
127
194
  var DEFAULT_GLOW_SIZE = 20;
128
195
  var DEFAULT_SKELETON_COLOR = "#ffffff";
@@ -137,7 +204,7 @@ var GLOW_INNER_EDGE = 0.4;
137
204
  var GLOW_FALLOFF_OPACITY = 0.53;
138
205
  function hexToRgbComponents(hex) {
139
206
  const n = parseInt(hex.slice(1), 16);
140
- return `${n >> 16},${n >> 8 & 255},${n & 255}`;
207
+ return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
141
208
  }
142
209
  function createRenderer(options) {
143
210
  const canvas = options.canvas;
@@ -151,7 +218,7 @@ function createRenderer(options) {
151
218
  trailColor: options.trailColor ?? "#ffffff",
152
219
  headColor: options.headColor ?? "#ffffff",
153
220
  headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
154
- glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE
221
+ glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE,
155
222
  };
156
223
  const trailRgb = hexToRgbComponents(opts.trailColor);
157
224
  const headRgbFalloff = `rgba(${hexToRgbComponents(opts.headColor)},${GLOW_FALLOFF_OPACITY})`;
@@ -165,12 +232,21 @@ function createRenderer(options) {
165
232
  let offsetY = 0;
166
233
  let animationId = null;
167
234
  let lastTime = 0;
235
+ let morphResolve = null;
236
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
237
+ let morphTarget = null;
238
+ let morphAlpha = 0;
239
+ let skeletonCanvasA = null;
240
+ let skeletonCanvasB = null;
168
241
  function calculateBoundaries() {
169
242
  if (skeleton.length === 0) {
170
243
  return;
171
244
  }
172
245
  const first = skeleton[0];
173
- let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
246
+ let minX = first.x,
247
+ maxX = first.x,
248
+ minY = first.y,
249
+ maxY = first.y;
174
250
  for (const p of skeleton) {
175
251
  if (p.x < minX) {
176
252
  minX = p.x;
@@ -216,6 +292,18 @@ function createRenderer(options) {
216
292
  if (opts.skeletonColor === "transparent") {
217
293
  return;
218
294
  }
295
+ if (engine.morphAlpha !== null) {
296
+ if (skeletonCanvasA) {
297
+ ctx.globalAlpha = (1 - morphAlpha) * DEFAULT_SKELETON_OPACITY;
298
+ ctx.drawImage(skeletonCanvasA, 0, 0);
299
+ }
300
+ if (skeletonCanvasB) {
301
+ ctx.globalAlpha = morphAlpha * DEFAULT_SKELETON_OPACITY;
302
+ ctx.drawImage(skeletonCanvasB, 0, 0);
303
+ }
304
+ ctx.globalAlpha = 1;
305
+ return;
306
+ }
219
307
  if (engine.isLiveSkeleton) {
220
308
  if (skeleton.length < 2) {
221
309
  return;
@@ -282,11 +370,26 @@ function createRenderer(options) {
282
370
  const now = performance.now();
283
371
  const deltaTime = Math.min((now - lastTime) / 1e3, 1 / 30);
284
372
  lastTime = now;
373
+ if (engine.morphAlpha !== null) {
374
+ morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
375
+ engine.setMorphAlpha(morphAlpha);
376
+ skeleton = engine.getSarmalSkeleton();
377
+ calculateBoundaries();
378
+ if (morphAlpha >= 1) {
379
+ engine.completeMorph();
380
+ morphResolve?.();
381
+ morphResolve = null;
382
+ morphTarget = null;
383
+ morphAlpha = 0;
384
+ skeletonCanvasA = null;
385
+ skeletonCanvasB = null;
386
+ }
387
+ }
285
388
  trail = engine.tick(deltaTime);
286
389
  trailCount = engine.trailCount;
287
390
  head = trailCount > 0 ? trail[trailCount - 1] : null;
288
391
  ctx.clearRect(0, 0, canvas.width, canvas.height);
289
- if (engine.isLiveSkeleton) {
392
+ if (engine.isLiveSkeleton || engine.morphAlpha !== null) {
290
393
  skeleton = engine.getSarmalSkeleton();
291
394
  calculateBoundaries();
292
395
  }
@@ -331,33 +434,85 @@ function createRenderer(options) {
331
434
  },
332
435
  seekWithTrail(t) {
333
436
  engine.seekWithTrail(t);
334
- }
437
+ },
438
+ morphTo(target, options2) {
439
+ if (morphResolve !== null) {
440
+ engine.completeMorph();
441
+ morphResolve();
442
+ morphResolve = null;
443
+ morphAlpha = 0;
444
+ skeletonCanvasA = null;
445
+ skeletonCanvasB = null;
446
+ }
447
+ morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
448
+ morphTarget = target;
449
+ morphAlpha = 0;
450
+ const currentSkeleton = engine.getSarmalSkeleton();
451
+ if (currentSkeleton.length >= 2) {
452
+ skeletonCanvasA = new OffscreenCanvas(canvas.width, canvas.height);
453
+ const ctxA = skeletonCanvasA.getContext("2d");
454
+ ctxA.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
455
+ ctxA.lineWidth = 1.5;
456
+ ctxA.beginPath();
457
+ const first = currentSkeleton[0];
458
+ ctxA.moveTo(first.x * scale + offsetX, first.y * scale + offsetY);
459
+ for (let i = 1; i < currentSkeleton.length; i++) {
460
+ const p = currentSkeleton[i];
461
+ ctxA.lineTo(p.x * scale + offsetX, p.y * scale + offsetY);
462
+ }
463
+ ctxA.stroke();
464
+ }
465
+ engine.startMorph(target, options2?.morphStrategy);
466
+ if (morphTarget && !engine.isLiveSkeleton) {
467
+ skeletonCanvasB = new OffscreenCanvas(canvas.width, canvas.height);
468
+ const skeletonCtx = skeletonCanvasB.getContext("2d");
469
+ skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
470
+ skeletonCtx.lineWidth = 1.5;
471
+ skeletonCtx.beginPath();
472
+ const period = morphTarget.period ?? Math.PI * 2;
473
+ const samples = Math.max(50, Math.round(period * 20));
474
+ const firstB = morphTarget.fn(0, 0, {});
475
+ skeletonCtx.moveTo(firstB.x * scale + offsetX, firstB.y * scale + offsetY);
476
+ for (let i = 1; i <= samples; i++) {
477
+ const t = (i / samples) * period;
478
+ const p = morphTarget.fn(t, 0, {});
479
+ skeletonCtx.lineTo(p.x * scale + offsetX, p.y * scale + offsetY);
480
+ }
481
+ skeletonCtx.stroke();
482
+ }
483
+ return new Promise((resolve) => {
484
+ morphResolve = resolve;
485
+ });
486
+ },
335
487
  };
336
488
  }
337
489
 
338
490
  // src/curves.ts
339
491
  var TWO_PI2 = Math.PI * 2;
340
492
  function artemis2(t, _time, _params) {
341
- const a = 0.35, b = 0.15, ox = 0.175;
342
- const s = Math.sin(t), c = Math.cos(t);
493
+ const a = 0.35,
494
+ b = 0.15,
495
+ ox = 0.175;
496
+ const s = Math.sin(t),
497
+ c = Math.cos(t);
343
498
  const denom = 1 + s * s;
344
499
  return {
345
- x: c * (1 + a * c) / denom - ox,
346
- y: s * c * (1 + b * c) / denom
500
+ x: (c * (1 + a * c)) / denom - ox,
501
+ y: (s * c * (1 + b * c)) / denom,
347
502
  };
348
503
  }
349
504
  function epitrochoid7(t, _time, _params) {
350
505
  const d = 1 + 0.55 * Math.sin(t * 0.5);
351
506
  return {
352
507
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
353
- y: 7 * Math.sin(t) - d * Math.sin(7 * t)
508
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t),
354
509
  };
355
510
  }
356
511
  function epitrochoid7Skeleton(t) {
357
512
  const d = 1.275;
358
513
  return {
359
514
  x: 7 * Math.cos(t) - d * Math.cos(7 * t),
360
- y: 7 * Math.sin(t) - d * Math.sin(7 * t)
515
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t),
361
516
  };
362
517
  }
363
518
  function astroid(t, _time, _params) {
@@ -365,55 +520,56 @@ function astroid(t, _time, _params) {
365
520
  const s = Math.sin(t);
366
521
  return {
367
522
  x: c * c * c,
368
- y: s * s * s
523
+ y: s * s * s,
369
524
  };
370
525
  }
371
526
  function deltoid(t, _time, _params) {
372
527
  return {
373
528
  x: 2 * Math.cos(t) + Math.cos(2 * t),
374
- y: 2 * Math.sin(t) - Math.sin(2 * t)
529
+ y: 2 * Math.sin(t) - Math.sin(2 * t),
375
530
  };
376
531
  }
377
532
  function rose5(t, _time, _params) {
378
533
  const r = Math.cos(5 * t);
379
534
  return {
380
535
  x: r * Math.cos(t),
381
- y: r * Math.sin(t)
536
+ y: r * Math.sin(t),
382
537
  };
383
538
  }
384
539
  function rose3(t, _time, _params) {
385
540
  const r = Math.cos(3 * t);
386
541
  return {
387
542
  x: r * Math.cos(t),
388
- y: r * Math.sin(t)
543
+ y: r * Math.sin(t),
389
544
  };
390
545
  }
391
546
  function lissajous32(t, time, _params) {
392
547
  const phi = time * 0.45;
393
548
  return {
394
549
  x: Math.sin(3 * t + phi),
395
- y: Math.sin(2 * t)
550
+ y: Math.sin(2 * t),
396
551
  };
397
552
  }
398
553
  function lissajous43(t, time, _params) {
399
554
  const phi = time * 0.38;
400
555
  return {
401
556
  x: Math.sin(4 * t + phi),
402
- y: Math.sin(3 * t)
557
+ y: Math.sin(3 * t),
403
558
  };
404
559
  }
405
560
  function epicycloid3(t, _time, _params) {
406
561
  return {
407
562
  x: 4 * Math.cos(t) - Math.cos(4 * t),
408
- y: 4 * Math.sin(t) - Math.sin(4 * t)
563
+ y: 4 * Math.sin(t) - Math.sin(4 * t),
409
564
  };
410
565
  }
411
566
  function lame(t, time, _params) {
412
567
  const p = 1.75 + 1.25 * Math.sin(time * 0.48);
413
- const c = Math.cos(t), s = Math.sin(t);
568
+ const c = Math.cos(t),
569
+ s = Math.sin(t);
414
570
  return {
415
571
  x: Math.sign(c) * Math.pow(Math.abs(c), p),
416
- y: Math.sign(s) * Math.pow(Math.abs(s), p)
572
+ y: Math.sign(s) * Math.pow(Math.abs(s), p),
417
573
  };
418
574
  }
419
575
  var curves = {
@@ -421,66 +577,66 @@ var curves = {
421
577
  name: "Artemis II",
422
578
  fn: artemis2,
423
579
  period: TWO_PI2,
424
- speed: 0.7
580
+ speed: 0.7,
425
581
  },
426
582
  epitrochoid7: {
427
583
  name: "Epitrochoid",
428
584
  fn: epitrochoid7,
429
585
  period: TWO_PI2,
430
586
  speed: 1.4,
431
- skeletonFn: epitrochoid7Skeleton
587
+ skeletonFn: epitrochoid7Skeleton,
432
588
  },
433
589
  astroid: {
434
590
  name: "Astroid",
435
591
  fn: astroid,
436
592
  period: TWO_PI2,
437
- speed: 1.1
593
+ speed: 1.1,
438
594
  },
439
595
  deltoid: {
440
596
  name: "Deltoid",
441
597
  fn: deltoid,
442
598
  period: TWO_PI2,
443
- speed: 0.9
599
+ speed: 0.9,
444
600
  },
445
601
  rose5: {
446
602
  name: "Rose (n=5)",
447
603
  fn: rose5,
448
604
  period: TWO_PI2,
449
- speed: 1
605
+ speed: 1,
450
606
  },
451
607
  rose3: {
452
608
  name: "Rose (n=3)",
453
609
  fn: rose3,
454
610
  period: TWO_PI2,
455
- speed: 1.15
611
+ speed: 1.15,
456
612
  },
457
613
  lissajous32: {
458
614
  name: "Lissajous 3:2",
459
615
  fn: lissajous32,
460
616
  period: TWO_PI2,
461
617
  speed: 2,
462
- skeleton: "live"
618
+ skeleton: "live",
463
619
  },
464
620
  lissajous43: {
465
621
  name: "Lissajous 4:3",
466
622
  fn: lissajous43,
467
623
  period: TWO_PI2,
468
624
  speed: 1.8,
469
- skeleton: "live"
625
+ skeleton: "live",
470
626
  },
471
627
  epicycloid3: {
472
628
  name: "Epicycloid (n=3)",
473
629
  fn: epicycloid3,
474
630
  period: TWO_PI2,
475
- speed: 0.75
631
+ speed: 0.75,
476
632
  },
477
633
  lame: {
478
634
  name: "Lam\xE9 Curve",
479
635
  fn: lame,
480
636
  period: TWO_PI2,
481
637
  speed: 1,
482
- skeleton: "live"
483
- }
638
+ skeleton: "live",
639
+ },
484
640
  };
485
641
 
486
642
  // src/index.ts
@@ -503,12 +659,12 @@ function init() {
503
659
  return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
504
660
  }
505
661
  const sarmal = createSarmal(canvas, curveDef, {
506
- ...canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor },
507
- ...canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor },
508
- ...canvas.dataset.headColor && { headColor: canvas.dataset.headColor },
509
- ...canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) },
510
- ...canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) },
511
- ...canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }
662
+ ...(canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor }),
663
+ ...(canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor }),
664
+ ...(canvas.dataset.headColor && { headColor: canvas.dataset.headColor }),
665
+ ...(canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) }),
666
+ ...(canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) }),
667
+ ...(canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }),
512
668
  });
513
669
  sarmal.start();
514
670
  });
@@ -519,4 +675,4 @@ if (document.readyState === "loading") {
519
675
  init();
520
676
  }
521
677
  //# sourceMappingURL=auto-init.cjs.map
522
- //# sourceMappingURL=auto-init.cjs.map
678
+ //# sourceMappingURL=auto-init.cjs.map