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