@sarmal/core 0.1.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 ADDED
@@ -0,0 +1,354 @@
1
+ 'use strict';
2
+
3
+ // src/engine.ts
4
+ var TWO_PI = Math.PI * 2;
5
+ var POINTS_PER_PERIOD_UNIT = 50;
6
+ var CircularBuffer = class {
7
+ constructor(capacity) {
8
+ this.head = 0;
9
+ this.count = 0;
10
+ this.capacity = capacity;
11
+ this.data = Array.from({ length: capacity }, () => ({ x: 0, y: 0 }));
12
+ }
13
+ /**
14
+ * Array elements are pre-allocated and `head` pointer is manually assigned,
15
+ * because AI said using `Array.shift` would be `O(n)` and this would be `O(1)`
16
+ * and I don't know any better.
17
+ */
18
+ push(x, y) {
19
+ this.data[this.head] = { x, y };
20
+ this.head = (this.head + 1) % this.capacity;
21
+ if (this.count < this.capacity) {
22
+ this.count++;
23
+ }
24
+ }
25
+ toArray() {
26
+ const result = new Array(this.count);
27
+ const start = this.count < this.capacity ? 0 : this.head;
28
+ for (let i = 0; i < this.count; i++) {
29
+ const index = (start + i) % this.capacity;
30
+ const p = this.data[index];
31
+ result[i] = { x: p.x, y: p.y };
32
+ }
33
+ return result;
34
+ }
35
+ clear() {
36
+ this.head = 0;
37
+ this.count = 0;
38
+ }
39
+ get length() {
40
+ return this.count;
41
+ }
42
+ };
43
+ function createEngine(curveDef, trailLength = 120) {
44
+ const curve = {
45
+ name: curveDef.name,
46
+ fn: curveDef.fn,
47
+ period: curveDef.period ?? TWO_PI,
48
+ speed: curveDef.speed ?? 1
49
+ };
50
+ const trail = new CircularBuffer(trailLength);
51
+ let t = 0;
52
+ let actualTime = 0;
53
+ return {
54
+ tick(deltaTime) {
55
+ t = (t + curve.speed * deltaTime) % curve.period;
56
+ actualTime += deltaTime;
57
+ const point = curve.fn(t, actualTime, {});
58
+ trail.push(point.x, point.y);
59
+ return trail.toArray();
60
+ },
61
+ reset() {
62
+ t = 0;
63
+ actualTime = 0;
64
+ trail.clear();
65
+ },
66
+ getSarmalSkeleton() {
67
+ const steps = Math.ceil(curve.period * POINTS_PER_PERIOD_UNIT);
68
+ const points = new Array(steps);
69
+ for (let i = 0; i < steps; i++) {
70
+ const sampleT = i / (steps - 1) * curve.period;
71
+ const point = curve.fn(sampleT, 0, {});
72
+ points[i] = point;
73
+ }
74
+ return points;
75
+ }
76
+ };
77
+ }
78
+
79
+ // src/renderer.ts
80
+ var DEFAULT_HEAD_RADIUS = 4;
81
+ var DEFAULT_GLOW_SIZE = 20;
82
+ var DEFAULT_SKELETON_COLOR = "#ffffff";
83
+ var DEFAULT_SKELETON_OPACITY = 0.15;
84
+ var FIT_PADDING = 0.1;
85
+ var TRAIL_BATCH_SIZE = 10;
86
+ var TRAIL_FADE_CURVE = 1.5;
87
+ var TRAIL_MAX_OPACITY = 0.88;
88
+ var TRAIL_MIN_WIDTH = 0.5;
89
+ var TRAIL_MAX_WIDTH = 2.5;
90
+ var GLOW_INNER_EDGE = 0.4;
91
+ var GLOW_FALLOFF_OPACITY = 0.53;
92
+ function hexToRgba(hex, alpha) {
93
+ const n = parseInt(hex.slice(1), 16);
94
+ return `rgba(${n >> 16},${n >> 8 & 255},${n & 255},${alpha})`;
95
+ }
96
+ function createRenderer(options) {
97
+ const canvas = options.canvas;
98
+ if (!canvas.getContext("2d")) {
99
+ throw new Error("Could not get 2d context from canvas");
100
+ }
101
+ const ctx = canvas.getContext("2d");
102
+ const engine = options.engine;
103
+ const opts = {
104
+ skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,
105
+ trailColor: options.trailColor ?? "#ffffff",
106
+ headColor: options.headColor ?? "#ffffff",
107
+ headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
108
+ glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE
109
+ };
110
+ let skeleton = [];
111
+ let trail = [];
112
+ let head = null;
113
+ let scale = 1;
114
+ let offsetX = 0;
115
+ let offsetY = 0;
116
+ let animationId = null;
117
+ let lastTime = 0;
118
+ function calculateBoundaries() {
119
+ if (skeleton.length === 0) {
120
+ return;
121
+ }
122
+ const first = skeleton[0];
123
+ let minX = first.x, maxX = first.x, minY = first.y, maxY = first.y;
124
+ for (const p of skeleton) {
125
+ if (p.x < minX) {
126
+ minX = p.x;
127
+ }
128
+ if (p.x > maxX) {
129
+ maxX = p.x;
130
+ }
131
+ if (p.y < minY) {
132
+ minY = p.y;
133
+ }
134
+ if (p.y > maxY) {
135
+ maxY = p.y;
136
+ }
137
+ }
138
+ const width = maxX - minX;
139
+ const height = maxY - minY;
140
+ const canvasWidth = canvas.width;
141
+ const canvasHeight = canvas.height;
142
+ const scaleX = canvasWidth / (width * (1 + FIT_PADDING * 2));
143
+ const scaleY = canvasHeight / (height * (1 + FIT_PADDING * 2));
144
+ scale = Math.min(scaleX, scaleY);
145
+ const boundsWidth = width * scale;
146
+ const boundsHeight = height * scale;
147
+ offsetX = (canvasWidth - boundsWidth) / 2 - minX * scale;
148
+ offsetY = (canvasHeight - boundsHeight) / 2 - minY * scale;
149
+ }
150
+ function transformCoordinateToPixel(p) {
151
+ return {
152
+ x: p.x * scale + offsetX,
153
+ y: p.y * scale + offsetY
154
+ };
155
+ }
156
+ function drawSkeleton() {
157
+ if (skeleton.length < 2) {
158
+ return;
159
+ }
160
+ ctx.strokeStyle = hexToRgba(opts.skeletonColor, DEFAULT_SKELETON_OPACITY);
161
+ ctx.lineWidth = 1.5;
162
+ ctx.beginPath();
163
+ const firstPixel = transformCoordinateToPixel(skeleton[0]);
164
+ ctx.moveTo(firstPixel.x, firstPixel.y);
165
+ for (let i = 1; i < skeleton.length; i++) {
166
+ const pixel = transformCoordinateToPixel(skeleton[i]);
167
+ ctx.lineTo(pixel.x, pixel.y);
168
+ }
169
+ ctx.stroke();
170
+ }
171
+ function drawTrail() {
172
+ if (trail.length < 2) {
173
+ return;
174
+ }
175
+ for (let b = 0; b < trail.length - 1; b += TRAIL_BATCH_SIZE) {
176
+ const bEnd = Math.min(b + TRAIL_BATCH_SIZE, trail.length - 1);
177
+ const progress = (b + bEnd) / 2 / (trail.length - 1);
178
+ const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
179
+ const lineWidth = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
180
+ ctx.beginPath();
181
+ for (let i = b; i <= bEnd; i++) {
182
+ const pixel = transformCoordinateToPixel(trail[i]);
183
+ if (i === b) {
184
+ ctx.moveTo(pixel.x, pixel.y);
185
+ } else {
186
+ ctx.lineTo(pixel.x, pixel.y);
187
+ }
188
+ }
189
+ ctx.strokeStyle = hexToRgba(opts.trailColor, alpha);
190
+ ctx.lineWidth = lineWidth;
191
+ ctx.lineJoin = "round";
192
+ ctx.lineCap = "round";
193
+ ctx.stroke();
194
+ }
195
+ }
196
+ function drawHead() {
197
+ if (!head) {
198
+ return;
199
+ }
200
+ const { x, y } = transformCoordinateToPixel(head);
201
+ const gradient = ctx.createRadialGradient(x, y, 0, x, y, opts.glowSize);
202
+ gradient.addColorStop(0, opts.headColor);
203
+ gradient.addColorStop(GLOW_INNER_EDGE, hexToRgba(opts.headColor, GLOW_FALLOFF_OPACITY));
204
+ gradient.addColorStop(1, "transparent");
205
+ ctx.fillStyle = gradient;
206
+ ctx.beginPath();
207
+ ctx.arc(x, y, opts.glowSize, 0, Math.PI * 2);
208
+ ctx.fill();
209
+ ctx.fillStyle = opts.headColor;
210
+ ctx.beginPath();
211
+ ctx.arc(x, y, opts.headRadius, 0, Math.PI * 2);
212
+ ctx.fill();
213
+ }
214
+ function render() {
215
+ const now = performance.now();
216
+ const deltaTime = (now - lastTime) / 1e3;
217
+ lastTime = now;
218
+ trail = engine.tick(deltaTime);
219
+ head = trail.length > 0 ? trail[trail.length - 1] : null;
220
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
221
+ drawSkeleton();
222
+ drawTrail();
223
+ drawHead();
224
+ animationId = requestAnimationFrame(render);
225
+ }
226
+ skeleton = engine.getSarmalSkeleton();
227
+ calculateBoundaries();
228
+ return {
229
+ start() {
230
+ if (animationId !== null) {
231
+ return;
232
+ }
233
+ lastTime = performance.now();
234
+ render();
235
+ },
236
+ stop() {
237
+ if (animationId === null) {
238
+ return;
239
+ }
240
+ cancelAnimationFrame(animationId);
241
+ animationId = null;
242
+ },
243
+ reset() {
244
+ engine.reset();
245
+ trail = [];
246
+ head = null;
247
+ },
248
+ destroy() {
249
+ if (animationId !== null) {
250
+ cancelAnimationFrame(animationId);
251
+ animationId = null;
252
+ }
253
+ }
254
+ };
255
+ }
256
+
257
+ // src/curves.ts
258
+ var TWO_PI2 = Math.PI * 2;
259
+ function epitrochoid7(t, _time, _params) {
260
+ const d = 1 + 0.55 * Math.sin(t * 0.5);
261
+ return {
262
+ x: 7 * Math.cos(t) - d * Math.cos(7 * t),
263
+ y: 7 * Math.sin(t) - d * Math.sin(7 * t)
264
+ };
265
+ }
266
+ function astroid(t, _time, _params) {
267
+ const c = Math.cos(t);
268
+ const s = Math.sin(t);
269
+ return {
270
+ x: c * c * c,
271
+ y: s * s * s
272
+ };
273
+ }
274
+ function deltoid(t, _time, _params) {
275
+ return {
276
+ x: 2 * Math.cos(t) + Math.cos(2 * t),
277
+ y: 2 * Math.sin(t) - Math.sin(2 * t)
278
+ };
279
+ }
280
+ function rose5(t, _time, _params) {
281
+ const r = Math.cos(5 * t);
282
+ return {
283
+ x: r * Math.cos(t),
284
+ y: r * Math.sin(t)
285
+ };
286
+ }
287
+ function rose3(t, _time, _params) {
288
+ const r = Math.cos(3 * t);
289
+ return {
290
+ x: r * Math.cos(t),
291
+ y: r * Math.sin(t)
292
+ };
293
+ }
294
+ function artemis2(t, _time, _params) {
295
+ const a = 0.35, b = 0.15, ox = 0.175;
296
+ const s = Math.sin(t), c = Math.cos(t);
297
+ const denom = 1 + s * s;
298
+ return {
299
+ x: c * (1 + a * c) / denom - ox,
300
+ y: s * c * (1 + b * c) / denom
301
+ };
302
+ }
303
+ var curves = {
304
+ epitrochoid7: {
305
+ name: "Epitrochoid",
306
+ fn: epitrochoid7,
307
+ period: TWO_PI2,
308
+ speed: 1.4
309
+ },
310
+ astroid: {
311
+ name: "Astroid",
312
+ fn: astroid,
313
+ period: TWO_PI2,
314
+ speed: 1.1
315
+ },
316
+ deltoid: {
317
+ name: "Deltoid",
318
+ fn: deltoid,
319
+ period: TWO_PI2,
320
+ speed: 0.9
321
+ },
322
+ rose5: {
323
+ name: "Rose (n=5)",
324
+ fn: rose5,
325
+ period: TWO_PI2,
326
+ speed: 1
327
+ },
328
+ rose3: {
329
+ name: "Rose (n=3)",
330
+ fn: rose3,
331
+ period: TWO_PI2,
332
+ speed: 1.15
333
+ },
334
+ artemis2: {
335
+ name: "Artemis II",
336
+ fn: artemis2,
337
+ period: TWO_PI2,
338
+ speed: 0.7
339
+ }
340
+ };
341
+
342
+ // src/index.ts
343
+ function createSarmal(canvas, curveDef, options) {
344
+ const { trailLength, ...rendererOpts } = options ?? {};
345
+ const engine = createEngine(curveDef, trailLength);
346
+ return createRenderer({ canvas, engine, ...rendererOpts });
347
+ }
348
+
349
+ exports.createEngine = createEngine;
350
+ exports.createRenderer = createRenderer;
351
+ exports.createSarmal = createSarmal;
352
+ exports.curves = curves;
353
+ //# sourceMappingURL=index.cjs.map
354
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine.ts","../src/renderer.ts","../src/curves.ts","../src/index.ts"],"names":["TWO_PI"],"mappings":";;;AAEA,IAAM,MAAA,GAAS,KAAK,EAAA,GAAK,CAAA;AACzB,IAAM,sBAAA,GAAyB,EAAA;AAM/B,IAAM,iBAAN,MAAqB;AAAA,EAMnB,YAAY,QAAA,EAAkB;AAH9B,IAAA,IAAA,CAAQ,IAAA,GAAe,CAAA;AACvB,IAAA,IAAA,CAAQ,KAAA,GAAgB,CAAA;AAGtB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,OAAO,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAE,CAAE,CAAA;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAA,CAAK,GAAW,CAAA,EAAiB;AAC/B,IAAA,IAAA,CAAK,KAAK,IAAA,CAAK,IAAI,CAAA,GAAI,EAAE,GAAM,CAAA,EAAK;AACpC,IAAA,IAAA,CAAK,IAAA,GAAA,CAAQ,IAAA,CAAK,IAAA,GAAO,CAAA,IAAK,IAAA,CAAK,QAAA;AACnC,IAAA,IAAI,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,QAAA,EAAU;AAC9B,MAAA,IAAA,CAAK,KAAA,EAAA;AAAA,IACP;AAAA,EACF;AAAA,EAEA,OAAA,GAAwB;AAEtB,IAAA,MAAM,MAAA,GAAuB,IAAI,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA;AACjD,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,QAAA,GAAW,IAAI,IAAA,CAAK,IAAA;AACpD,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,OAAO,CAAA,EAAA,EAAK;AACnC,MAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,CAAA,IAAK,IAAA,CAAK,QAAA;AACjC,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA;AACzB,MAAA,MAAA,CAAO,CAAC,IAAI,EAAE,CAAA,EAAG,EAAE,CAAA,EAAG,CAAA,EAAG,EAAE,CAAA,EAAE;AAAA,IAC/B;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,CAAA;AACZ,IAAA,IAAA,CAAK,KAAA,GAAQ,CAAA;AAAA,EACf;AAAA,EAEA,IAAI,MAAA,GAAiB;AACnB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF,CAAA;AAcO,SAAS,YAAA,CAAa,QAAA,EAAoB,WAAA,GAAsB,GAAA,EAAa;AAClF,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,MAAM,QAAA,CAAS,IAAA;AAAA,IACf,IAAI,QAAA,CAAS,EAAA;AAAA,IACb,MAAA,EAAQ,SAAS,MAAA,IAAU,MAAA;AAAA,IAC3B,KAAA,EAAO,SAAS,KAAA,IAAS;AAAA,GAC3B;AACA,EAAA,MAAM,KAAA,GAAQ,IAAI,cAAA,CAAe,WAAW,CAAA;AAC5C,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,EAAA,OAAO;AAAA,IACL,KAAK,SAAA,EAAiC;AACpC,MAAA,CAAA,GAAA,CAAK,CAAA,GAAI,KAAA,CAAM,KAAA,GAAQ,SAAA,IAAa,KAAA,CAAM,MAAA;AAC1C,MAAA,UAAA,IAAc,SAAA;AACd,MAAA,MAAM,QAAQ,KAAA,CAAM,EAAA,CAAG,CAAA,EAAG,UAAA,EAAY,EAAE,CAAA;AACxC,MAAA,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA;AAC3B,MAAA,OAAO,MAAM,OAAA,EAAQ;AAAA,IACvB,CAAA;AAAA,IAEA,KAAA,GAAc;AACZ,MAAA,CAAA,GAAI,CAAA;AACJ,MAAA,UAAA,GAAa,CAAA;AACb,MAAA,KAAA,CAAM,KAAA,EAAM;AAAA,IACd,CAAA;AAAA,IAEA,iBAAA,GAAkC;AAChC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,SAAS,sBAAsB,CAAA;AAE7D,MAAA,MAAM,MAAA,GAAuB,IAAI,KAAA,CAAM,KAAK,CAAA;AAC5C,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,EAAO,CAAA,EAAA,EAAK;AAC9B,QAAA,MAAM,OAAA,GAAW,CAAA,IAAK,KAAA,GAAQ,CAAA,CAAA,GAAM,KAAA,CAAM,MAAA;AAC1C,QAAA,MAAM,QAAQ,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAA,EAAG,EAAE,CAAA;AACrC,QAAA,MAAA,CAAO,CAAC,CAAA,GAAI,KAAA;AAAA,MACd;AACA,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,GACF;AACF;;;ACvGA,IAAM,mBAAA,GAAsB,CAAA;AAC5B,IAAM,iBAAA,GAAoB,EAAA;AAC1B,IAAM,sBAAA,GAAyB,SAAA;AAC/B,IAAM,wBAAA,GAA2B,IAAA;AAGjC,IAAM,WAAA,GAAc,GAAA;AAOpB,IAAM,gBAAA,GAAmB,EAAA;AAEzB,IAAM,gBAAA,GAAmB,GAAA;AACzB,IAAM,iBAAA,GAAoB,IAAA;AAE1B,IAAM,eAAA,GAAkB,GAAA;AAExB,IAAM,eAAA,GAAkB,GAAA;AAExB,IAAM,eAAA,GAAkB,GAAA;AAExB,IAAM,oBAAA,GAAuB,IAAA;AAG7B,SAAS,SAAA,CAAU,KAAa,KAAA,EAAuB;AACrD,EAAA,MAAM,IAAI,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA;AACnC,EAAA,OAAO,CAAA,KAAA,EAAQ,CAAA,IAAK,EAAE,CAAA,CAAA,EAAK,CAAA,IAAK,CAAA,GAAK,GAAG,CAAA,CAAA,EAAI,CAAA,GAAI,GAAG,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,CAAA;AAC9D;AAMO,SAAS,eAAe,OAAA,EAA0C;AACvE,EAAA,MAAM,SAAS,OAAA,CAAQ,MAAA;AACvB,EAAA,IAAI,CAAC,MAAA,CAAO,UAAA,CAAW,IAAI,CAAA,EAAG;AAC5B,IAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,EACxD;AACA,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,UAAA,CAAW,IAAI,CAAA;AAElC,EAAA,MAAM,SAAS,OAAA,CAAQ,MAAA;AACvB,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,aAAA,EAAe,QAAQ,aAAA,IAAiB,sBAAA;AAAA,IACxC,UAAA,EAAY,QAAQ,UAAA,IAAc,SAAA;AAAA,IAClC,SAAA,EAAW,QAAQ,SAAA,IAAa,SAAA;AAAA,IAChC,UAAA,EAAY,QAAQ,UAAA,IAAc,mBAAA;AAAA,IAClC,QAAA,EAAU,QAAQ,QAAA,IAAY;AAAA,GAChC;AAEA,EAAA,IAAI,WAAyB,EAAC;AAC9B,EAAA,IAAI,QAAsB,EAAC;AAC3B,EAAA,IAAI,IAAA,GAAqB,IAAA;AACzB,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,IAAI,WAAA,GAA6B,IAAA;AACjC,EAAA,IAAI,QAAA,GAAW,CAAA;AAWf,EAAA,SAAS,mBAAA,GAAsB;AAC7B,IAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AACzB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAAQ,SAAS,CAAC,CAAA;AACxB,IAAA,IAAI,IAAA,GAAO,KAAA,CAAM,CAAA,EACf,IAAA,GAAO,KAAA,CAAM,GACb,IAAA,GAAO,KAAA,CAAM,CAAA,EACb,IAAA,GAAO,KAAA,CAAM,CAAA;AACf,IAAA,KAAA,MAAW,KAAK,QAAA,EAAU;AACxB,MAAA,IAAI,CAAA,CAAE,IAAI,IAAA,EAAM;AACd,QAAA,IAAA,GAAO,CAAA,CAAE,CAAA;AAAA,MACX;AAEA,MAAA,IAAI,CAAA,CAAE,IAAI,IAAA,EAAM;AACd,QAAA,IAAA,GAAO,CAAA,CAAE,CAAA;AAAA,MACX;AAEA,MAAA,IAAI,CAAA,CAAE,IAAI,IAAA,EAAM;AACd,QAAA,IAAA,GAAO,CAAA,CAAE,CAAA;AAAA,MACX;AAEA,MAAA,IAAI,CAAA,CAAE,IAAI,IAAA,EAAM;AACd,QAAA,IAAA,GAAO,CAAA,CAAE,CAAA;AAAA,MACX;AAAA,IACF;AAEA,IAAA,MAAM,QAAQ,IAAA,GAAO,IAAA;AACrB,IAAA,MAAM,SAAS,IAAA,GAAO,IAAA;AACtB,IAAA,MAAM,cAAc,MAAA,CAAO,KAAA;AAC3B,IAAA,MAAM,eAAe,MAAA,CAAO,MAAA;AAE5B,IAAA,MAAM,MAAA,GAAS,WAAA,IAAe,KAAA,IAAS,CAAA,GAAI,WAAA,GAAc,CAAA,CAAA,CAAA;AACzD,IAAA,MAAM,MAAA,GAAS,YAAA,IAAgB,MAAA,IAAU,CAAA,GAAI,WAAA,GAAc,CAAA,CAAA,CAAA;AAC3D,IAAA,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,MAAM,CAAA;AAE/B,IAAA,MAAM,cAAc,KAAA,GAAQ,KAAA;AAC5B,IAAA,MAAM,eAAe,MAAA,GAAS,KAAA;AAC9B,IAAA,OAAA,GAAA,CAAW,WAAA,GAAc,WAAA,IAAe,CAAA,GAAI,IAAA,GAAO,KAAA;AACnD,IAAA,OAAA,GAAA,CAAW,YAAA,GAAe,YAAA,IAAgB,CAAA,GAAI,IAAA,GAAO,KAAA;AAAA,EACvD;AAEA,EAAA,SAAS,2BAA2B,CAAA,EAAU;AAC5C,IAAA,OAAO;AAAA,MACL,CAAA,EAAG,CAAA,CAAE,CAAA,GAAI,KAAA,GAAQ,OAAA;AAAA,MACjB,CAAA,EAAG,CAAA,CAAE,CAAA,GAAI,KAAA,GAAQ;AAAA,KACnB;AAAA,EACF;AAEA,EAAA,SAAS,YAAA,GAAe;AACtB,IAAA,IAAI,QAAA,CAAS,SAAS,CAAA,EAAG;AACvB,MAAA;AAAA,IACF;AAEA,IAAA,GAAA,CAAI,WAAA,GAAc,SAAA,CAAU,IAAA,CAAK,aAAA,EAAe,wBAAwB,CAAA;AACxE,IAAA,GAAA,CAAI,SAAA,GAAY,GAAA;AAChB,IAAA,GAAA,CAAI,SAAA,EAAU;AAEd,IAAA,MAAM,UAAA,GAAa,0BAAA,CAA2B,QAAA,CAAS,CAAC,CAAE,CAAA;AAC1D,IAAA,GAAA,CAAI,MAAA,CAAO,UAAA,CAAW,CAAA,EAAG,UAAA,CAAW,CAAC,CAAA;AAErC,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,QAAQ,CAAA,EAAA,EAAK;AACxC,MAAA,MAAM,KAAA,GAAQ,0BAAA,CAA2B,QAAA,CAAS,CAAC,CAAE,CAAA;AACrD,MAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA;AAAA,IAC7B;AAEA,IAAA,GAAA,CAAI,MAAA,EAAO;AAAA,EACb;AAEA,EAAA,SAAS,SAAA,GAAY;AACnB,IAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,MAAM,MAAA,GAAS,CAAA,EAAG,KAAK,gBAAA,EAAkB;AAC3D,MAAA,MAAM,OAAO,IAAA,CAAK,GAAA,CAAI,IAAI,gBAAA,EAAkB,KAAA,CAAM,SAAS,CAAC,CAAA;AAE5D,MAAA,MAAM,QAAA,GAAA,CAAY,CAAA,GAAI,IAAA,IAAQ,CAAA,IAAK,MAAM,MAAA,GAAS,CAAA,CAAA;AAClD,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,gBAAgB,CAAA,GAAI,iBAAA;AACrD,MAAA,MAAM,SAAA,GAAY,eAAA,GAAkB,QAAA,IAAY,eAAA,GAAkB,eAAA,CAAA;AAElE,MAAA,GAAA,CAAI,SAAA,EAAU;AACd,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,IAAK,IAAA,EAAM,CAAA,EAAA,EAAK;AAC9B,QAAA,MAAM,KAAA,GAAQ,0BAAA,CAA2B,KAAA,CAAM,CAAC,CAAE,CAAA;AAClD,QAAA,IAAI,MAAM,CAAA,EAAG;AACX,UAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA;AAAA,QAC7B,CAAA,MAAO;AACL,UAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,KAAA,CAAM,CAAC,CAAA;AAAA,QAC7B;AAAA,MACF;AAEA,MAAA,GAAA,CAAI,WAAA,GAAc,SAAA,CAAU,IAAA,CAAK,UAAA,EAAY,KAAK,CAAA;AAClD,MAAA,GAAA,CAAI,SAAA,GAAY,SAAA;AAChB,MAAA,GAAA,CAAI,QAAA,GAAW,OAAA;AACf,MAAA,GAAA,CAAI,OAAA,GAAU,OAAA;AACd,MAAA,GAAA,CAAI,MAAA,EAAO;AAAA,IACb;AAAA,EACF;AAEA,EAAA,SAAS,QAAA,GAAW;AAClB,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,CAAA,EAAG,CAAA,EAAE,GAAI,2BAA2B,IAAI,CAAA;AAEhD,IAAA,MAAM,QAAA,GAAW,IAAI,oBAAA,CAAqB,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,QAAQ,CAAA;AACtE,IAAA,QAAA,CAAS,YAAA,CAAa,CAAA,EAAG,IAAA,CAAK,SAAS,CAAA;AACvC,IAAA,QAAA,CAAS,aAAa,eAAA,EAAiB,SAAA,CAAU,IAAA,CAAK,SAAA,EAAW,oBAAoB,CAAC,CAAA;AACtF,IAAA,QAAA,CAAS,YAAA,CAAa,GAAG,aAAa,CAAA;AAEtC,IAAA,GAAA,CAAI,SAAA,GAAY,QAAA;AAChB,IAAA,GAAA,CAAI,SAAA,EAAU;AACd,IAAA,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,EAAG,IAAA,CAAK,UAAU,CAAA,EAAG,IAAA,CAAK,KAAK,CAAC,CAAA;AAC3C,IAAA,GAAA,CAAI,IAAA,EAAK;AAET,IAAA,GAAA,CAAI,YAAY,IAAA,CAAK,SAAA;AACrB,IAAA,GAAA,CAAI,SAAA,EAAU;AACd,IAAA,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,EAAG,IAAA,CAAK,YAAY,CAAA,EAAG,IAAA,CAAK,KAAK,CAAC,CAAA;AAC7C,IAAA,GAAA,CAAI,IAAA,EAAK;AAAA,EACX;AAEA,EAAA,SAAS,MAAA,GAAS;AAChB,IAAA,MAAM,GAAA,GAAM,YAAY,GAAA,EAAI;AAC5B,IAAA,MAAM,SAAA,GAAA,CAAa,MAAM,QAAA,IAAY,GAAA;AACrC,IAAA,QAAA,GAAW,GAAA;AAEX,IAAA,KAAA,GAAQ,MAAA,CAAO,KAAK,SAAS,CAAA;AAC7B,IAAA,IAAA,GAAO,MAAM,MAAA,GAAS,CAAA,GAAI,MAAM,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,GAAK,IAAA;AAErD,IAAA,GAAA,CAAI,UAAU,CAAA,EAAG,CAAA,EAAG,MAAA,CAAO,KAAA,EAAO,OAAO,MAAM,CAAA;AAE/C,IAAA,YAAA,EAAa;AACb,IAAA,SAAA,EAAU;AACV,IAAA,QAAA,EAAS;AAET,IAAA,WAAA,GAAc,sBAAsB,MAAM,CAAA;AAAA,EAC5C;AAGA,EAAA,QAAA,GAAW,OAAO,iBAAA,EAAkB;AACpC,EAAA,mBAAA,EAAoB;AAEpB,EAAA,OAAO;AAAA,IACL,KAAA,GAAQ;AACN,MAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,QAAA;AAAA,MACF;AAEA,MAAA,QAAA,GAAW,YAAY,GAAA,EAAI;AAC3B,MAAA,MAAA,EAAO;AAAA,IACT,CAAA;AAAA,IAEA,IAAA,GAAO;AACL,MAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,QAAA;AAAA,MACF;AACA,MAAA,oBAAA,CAAqB,WAAW,CAAA;AAChC,MAAA,WAAA,GAAc,IAAA;AAAA,IAChB,CAAA;AAAA,IAEA,KAAA,GAAQ;AACN,MAAA,MAAA,CAAO,KAAA,EAAM;AACb,MAAA,KAAA,GAAQ,EAAC;AACT,MAAA,IAAA,GAAO,IAAA;AAAA,IACT,CAAA;AAAA,IAEA,OAAA,GAAU;AACR,MAAA,IAAI,gBAAgB,IAAA,EAAM;AACxB,QAAA,oBAAA,CAAqB,WAAW,CAAA;AAChC,QAAA,WAAA,GAAc,IAAA;AAAA,MAChB;AAAA,IACF;AAAA,GACF;AACF;;;ACrPA,IAAMA,OAAAA,GAAS,KAAK,EAAA,GAAK,CAAA;AAGzB,SAAS,YAAA,CAAa,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AACtF,EAAA,MAAM,IAAI,CAAA,GAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,IAAI,GAAG,CAAA;AACvC,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,IAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA;AAAA,IACvC,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,IAAI,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC;AAAA,GACzC;AACF;AAEA,SAAS,OAAA,CAAQ,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AACjF,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AACpB,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AACpB,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,IAAI,CAAA,GAAI,CAAA;AAAA,IACX,CAAA,EAAG,IAAI,CAAA,GAAI;AAAA,GACb;AACF;AAEA,SAAS,OAAA,CAAQ,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AACjF,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,IAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA;AAAA,IACnC,CAAA,EAAG,IAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC;AAAA,GACrC;AACF;AAEA,SAAS,KAAA,CAAM,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AAC/E,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA;AACxB,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AAAA,IACjB,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC;AAAA,GACnB;AACF;AAEA,SAAS,KAAA,CAAM,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AAC/E,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA;AACxB,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AAAA,IACjB,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC;AAAA,GACnB;AACF;AASA,SAAS,QAAA,CAAS,CAAA,EAAW,KAAA,EAAe,OAAA,EAAwC;AAClF,EAAA,MAAM,CAAA,GAAI,IAAA,EACR,CAAA,GAAI,IAAA,EACJ,EAAA,GAAK,KAAA;AACP,EAAA,MAAM,CAAA,GAAI,KAAK,GAAA,CAAI,CAAC,GAClB,CAAA,GAAI,IAAA,CAAK,IAAI,CAAC,CAAA;AAChB,EAAA,MAAM,KAAA,GAAQ,IAAI,CAAA,GAAI,CAAA;AACtB,EAAA,OAAO;AAAA,IACL,CAAA,EAAI,CAAA,IAAK,CAAA,GAAI,CAAA,GAAI,KAAM,KAAA,GAAQ,EAAA;AAAA,IAC/B,CAAA,EAAI,CAAA,GAAI,CAAA,IAAK,CAAA,GAAI,IAAI,CAAA,CAAA,GAAM;AAAA,GAC7B;AACF;AAEO,IAAM,MAAA,GAAmC;AAAA,EAC9C,YAAA,EAAc;AAAA,IACZ,IAAA,EAAM,aAAA;AAAA,IACN,EAAA,EAAI,YAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,SAAA;AAAA,IACN,EAAA,EAAI,OAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,SAAA;AAAA,IACN,EAAA,EAAI,OAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,EAAA,EAAI,KAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,EAAA,EAAI,KAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAAA,EACA,QAAA,EAAU;AAAA,IACR,IAAA,EAAM,YAAA;AAAA,IACN,EAAA,EAAI,QAAA;AAAA,IACJ,MAAA,EAAQA,OAAAA;AAAA,IACR,KAAA,EAAO;AAAA;AAEX;;;AC3EO,SAAS,YAAA,CACd,MAAA,EACA,QAAA,EACA,OAAA,EACgB;AAChB,EAAA,MAAM,EAAE,WAAA,EAAa,GAAG,YAAA,EAAa,GAAI,WAAW,EAAC;AACrD,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,QAAA,EAAU,WAAW,CAAA;AAEjD,EAAA,OAAO,eAAe,EAAE,MAAA,EAAQ,MAAA,EAAQ,GAAG,cAAc,CAAA;AAC3D","file":"index.cjs","sourcesContent":["import type { CurveDef, Engine, Point } from \"./types\";\n\nconst TWO_PI = Math.PI * 2;\nconst POINTS_PER_PERIOD_UNIT = 50;\n\n/**\n * A fixed-size list of points with first in, last out method\n * The oldest entry is automatically discarded when the list is at capacity\n */\nclass CircularBuffer {\n private data: Array<Point>;\n private capacity: number;\n private head: number = 0;\n private count: number = 0;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n this.data = Array.from({ length: capacity }, () => ({ x: 0, y: 0 }));\n }\n\n /**\n * Array elements are pre-allocated and `head` pointer is manually assigned,\n * because AI said using `Array.shift` would be `O(n)` and this would be `O(1)`\n * and I don't know any better.\n */\n push(x: number, y: number): void {\n this.data[this.head] = { x: x, y: y };\n this.head = (this.head + 1) % this.capacity;\n if (this.count < this.capacity) {\n this.count++;\n }\n }\n\n toArray(): Array<Point> {\n // oxlint-disable-next-line unicorn/no-new-array -- AI said it is pre-allocated for performance (runs every frame)\n const result: Array<Point> = new Array(this.count);\n const start = this.count < this.capacity ? 0 : this.head;\n for (let i = 0; i < this.count; i++) {\n const index = (start + i) % this.capacity;\n const p = this.data[index]!;\n result[i] = { x: p.x, y: p.y };\n }\n return result;\n }\n\n clear(): void {\n this.head = 0;\n this.count = 0;\n }\n\n get length(): number {\n return this.count;\n }\n}\n\n/**\n * Creates the core simulation engine for a sarmal\n *\n * it runs a clock (time `t`), asks the curve for the current Point position at that time,\n * and remembers the last N positions so the renderer can draw the trail\n *\n * The engine is only responsible for math coordinates,\n * so it is not responsible for drawing or colors\n *\n * @param curveDef A curve definition\n * @param trailLength default: `120`\n */\nexport function createEngine(curveDef: CurveDef, trailLength: number = 120): Engine {\n const curve = {\n name: curveDef.name,\n fn: curveDef.fn,\n period: curveDef.period ?? TWO_PI,\n speed: curveDef.speed ?? 1,\n };\n const trail = new CircularBuffer(trailLength);\n let t = 0;\n let actualTime = 0;\n\n return {\n tick(deltaTime: number): Array<Point> {\n t = (t + curve.speed * deltaTime) % curve.period;\n actualTime += deltaTime;\n const point = curve.fn(t, actualTime, {});\n trail.push(point.x, point.y);\n return trail.toArray();\n },\n\n reset(): void {\n t = 0;\n actualTime = 0;\n trail.clear();\n },\n\n getSarmalSkeleton(): Array<Point> {\n const steps = Math.ceil(curve.period * POINTS_PER_PERIOD_UNIT);\n // oxlint-disable-next-line unicorn/no-new-array -- array is pre-allocated, filled immediately below\n const points: Array<Point> = new Array(steps);\n for (let i = 0; i < steps; i++) {\n const sampleT = (i / (steps - 1)) * curve.period;\n const point = curve.fn(sampleT, 0, {});\n points[i] = point;\n }\n return points;\n },\n };\n}\n","import type { Point, RendererOptions, SarmalInstance } from \"./types\";\n\nconst DEFAULT_HEAD_RADIUS = 4;\nconst DEFAULT_GLOW_SIZE = 20;\nconst DEFAULT_SKELETON_COLOR = \"#ffffff\";\nconst DEFAULT_SKELETON_OPACITY = 0.15;\n\n/** Fraction of the bounding box added as padding when fitting the curve to the canvas */\nconst FIT_PADDING = 0.1;\n\n/**\n * The trail is drawn in batches of points\n * Each batch has lower opacity than the one that comes before it\n * (0 = oldest/tail, 1 = newest/head)\n */\nconst TRAIL_BATCH_SIZE = 10;\n/** Higher values = sharper fade near the tail, more of the trail appears faint */\nconst TRAIL_FADE_CURVE = 1.5;\nconst TRAIL_MAX_OPACITY = 0.88;\n/** Line width of tail */\nconst TRAIL_MIN_WIDTH = 0.5;\n/** Line width of head */\nconst TRAIL_MAX_WIDTH = 2.5;\n\nconst GLOW_INNER_EDGE = 0.4;\n/** Opacity at the inner edge of the glow falloff */\nconst GLOW_FALLOFF_OPACITY = 0.53;\n\n// TODO: might as well accept rgb/rgba directly too\nfunction hexToRgba(hex: string, alpha: number): string {\n const n = parseInt(hex.slice(1), 16);\n return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${alpha})`;\n}\n\n/**\n * Creates a Canvas 2D renderer for sarmal animations\n * Renders the skeleton, the trail, and the glowing dot.\n */\nexport function createRenderer(options: RendererOptions): SarmalInstance {\n const canvas = options.canvas;\n if (!canvas.getContext(\"2d\")) {\n throw new Error(\"Could not get 2d context from canvas\");\n }\n const ctx = canvas.getContext(\"2d\")!;\n\n const engine = options.engine;\n const opts = {\n skeletonColor: options.skeletonColor ?? DEFAULT_SKELETON_COLOR,\n trailColor: options.trailColor ?? \"#ffffff\",\n headColor: options.headColor ?? \"#ffffff\",\n headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,\n glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE,\n };\n\n let skeleton: Array<Point> = [];\n let trail: Array<Point> = [];\n let head: Point | null = null;\n let scale = 1;\n let offsetX = 0;\n let offsetY = 0;\n let animationId: number | null = null;\n let lastTime = 0;\n\n /**\n * Computes how to map engine coordinates to canvas pixels\n *\n * Steps are roughly: curve fn -> coordinate point -> (scale + offset) -> pixel\n *\n * 1. Find the bounding box of the skeleton (min/max x/y in coordinates)\n * 2. Compute a scale factor within the bounds into the canvas with padding\n * 3. Compute offsets to center the curve in the canvas\n */\n function calculateBoundaries() {\n if (skeleton.length === 0) {\n return;\n }\n\n const first = skeleton[0]!;\n let minX = first.x,\n maxX = first.x,\n minY = first.y,\n maxY = first.y;\n for (const p of skeleton) {\n if (p.x < minX) {\n minX = p.x;\n }\n\n if (p.x > maxX) {\n maxX = p.x;\n }\n\n if (p.y < minY) {\n minY = p.y;\n }\n\n if (p.y > maxY) {\n maxY = p.y;\n }\n }\n\n const width = maxX - minX;\n const height = maxY - minY;\n const canvasWidth = canvas.width;\n const canvasHeight = canvas.height;\n\n const scaleX = canvasWidth / (width * (1 + FIT_PADDING * 2));\n const scaleY = canvasHeight / (height * (1 + FIT_PADDING * 2));\n scale = Math.min(scaleX, scaleY);\n\n const boundsWidth = width * scale;\n const boundsHeight = height * scale;\n offsetX = (canvasWidth - boundsWidth) / 2 - minX * scale;\n offsetY = (canvasHeight - boundsHeight) / 2 - minY * scale;\n }\n\n function transformCoordinateToPixel(p: Point) {\n return {\n x: p.x * scale + offsetX,\n y: p.y * scale + offsetY,\n };\n }\n\n function drawSkeleton() {\n if (skeleton.length < 2) {\n return;\n }\n\n ctx.strokeStyle = hexToRgba(opts.skeletonColor, DEFAULT_SKELETON_OPACITY);\n ctx.lineWidth = 1.5;\n ctx.beginPath();\n\n const firstPixel = transformCoordinateToPixel(skeleton[0]!);\n ctx.moveTo(firstPixel.x, firstPixel.y);\n\n for (let i = 1; i < skeleton.length; i++) {\n const pixel = transformCoordinateToPixel(skeleton[i]!);\n ctx.lineTo(pixel.x, pixel.y);\n }\n\n ctx.stroke();\n }\n\n function drawTrail() {\n if (trail.length < 2) {\n return;\n }\n\n for (let b = 0; b < trail.length - 1; b += TRAIL_BATCH_SIZE) {\n const bEnd = Math.min(b + TRAIL_BATCH_SIZE, trail.length - 1);\n /** Normalized position of this batch along the trail (0 = tail, 1 = head) */\n const progress = (b + bEnd) / 2 / (trail.length - 1);\n const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;\n const lineWidth = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);\n\n ctx.beginPath();\n for (let i = b; i <= bEnd; i++) {\n const pixel = transformCoordinateToPixel(trail[i]!);\n if (i === b) {\n ctx.moveTo(pixel.x, pixel.y);\n } else {\n ctx.lineTo(pixel.x, pixel.y);\n }\n }\n\n ctx.strokeStyle = hexToRgba(opts.trailColor, alpha);\n ctx.lineWidth = lineWidth;\n ctx.lineJoin = \"round\";\n ctx.lineCap = \"round\";\n ctx.stroke();\n }\n }\n\n function drawHead() {\n if (!head) {\n return;\n }\n\n const { x, y } = transformCoordinateToPixel(head);\n\n const gradient = ctx.createRadialGradient(x, y, 0, x, y, opts.glowSize);\n gradient.addColorStop(0, opts.headColor);\n gradient.addColorStop(GLOW_INNER_EDGE, hexToRgba(opts.headColor, GLOW_FALLOFF_OPACITY));\n gradient.addColorStop(1, \"transparent\");\n\n ctx.fillStyle = gradient;\n ctx.beginPath();\n ctx.arc(x, y, opts.glowSize, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = opts.headColor;\n ctx.beginPath();\n ctx.arc(x, y, opts.headRadius, 0, Math.PI * 2);\n ctx.fill();\n }\n\n function render() {\n const now = performance.now();\n const deltaTime = (now - lastTime) / 1000;\n lastTime = now;\n\n trail = engine.tick(deltaTime);\n head = trail.length > 0 ? trail[trail.length - 1]! : null;\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n drawSkeleton();\n drawTrail();\n drawHead();\n\n animationId = requestAnimationFrame(render);\n }\n\n // Initialize skeleton on creation\n skeleton = engine.getSarmalSkeleton();\n calculateBoundaries();\n\n return {\n start() {\n if (animationId !== null) {\n return;\n }\n\n lastTime = performance.now();\n render();\n },\n\n stop() {\n if (animationId === null) {\n return;\n }\n cancelAnimationFrame(animationId);\n animationId = null;\n },\n\n reset() {\n engine.reset();\n trail = [];\n head = null;\n },\n\n destroy() {\n if (animationId !== null) {\n cancelAnimationFrame(animationId);\n animationId = null;\n }\n },\n };\n}\n","import type { CurveDef, Point } from \"./types\";\n\nconst TWO_PI = Math.PI * 2;\n\n/** 7-lobed epitrochoid with a breathing distance parameter — d oscillates with t, making the loops pulse in and out */\nfunction epitrochoid7(t: number, _time: number, _params: Record<string, number>): Point {\n const d = 1.0 + 0.55 * Math.sin(t * 0.5);\n return {\n x: 7 * Math.cos(t) - d * Math.cos(7 * t),\n y: 7 * Math.sin(t) - d * Math.sin(7 * t),\n };\n}\n\nfunction astroid(t: number, _time: number, _params: Record<string, number>): Point {\n const c = Math.cos(t);\n const s = Math.sin(t);\n return {\n x: c * c * c,\n y: s * s * s,\n };\n}\n\nfunction deltoid(t: number, _time: number, _params: Record<string, number>): Point {\n return {\n x: 2 * Math.cos(t) + Math.cos(2 * t),\n y: 2 * Math.sin(t) - Math.sin(2 * t),\n };\n}\n\nfunction rose5(t: number, _time: number, _params: Record<string, number>): Point {\n const r = Math.cos(5 * t);\n return {\n x: r * Math.cos(t),\n y: r * Math.sin(t),\n };\n}\n\nfunction rose3(t: number, _time: number, _params: Record<string, number>): Point {\n const r = Math.cos(3 * t);\n return {\n x: r * Math.cos(t),\n y: r * Math.sin(t),\n };\n}\n\n/**\n * Artemis II free-return lunar trajectory\n * @see https://www.nasa.gov/wp-content/uploads/2025/09/artemis-ii-map-508.pdf\n * a = x-axis asymmetry (widens one lobe),\n * b = y-axis asymmetry,\n * ox = horizontal offset to visually center the shape\n */\nfunction artemis2(t: number, _time: number, _params: Record<string, number>): Point {\n const a = 0.35,\n b = 0.15,\n ox = 0.175;\n const s = Math.sin(t),\n c = Math.cos(t);\n const denom = 1 + s * s;\n return {\n x: (c * (1 + a * c)) / denom - ox,\n y: (s * c * (1 + b * c)) / denom,\n };\n}\n\nexport const curves: Record<string, CurveDef> = {\n epitrochoid7: {\n name: \"Epitrochoid\",\n fn: epitrochoid7,\n period: TWO_PI,\n speed: 1.4,\n },\n astroid: {\n name: \"Astroid\",\n fn: astroid,\n period: TWO_PI,\n speed: 1.1,\n },\n deltoid: {\n name: \"Deltoid\",\n fn: deltoid,\n period: TWO_PI,\n speed: 0.9,\n },\n rose5: {\n name: \"Rose (n=5)\",\n fn: rose5,\n period: TWO_PI,\n speed: 1.0,\n },\n rose3: {\n name: \"Rose (n=3)\",\n fn: rose3,\n period: TWO_PI,\n speed: 1.15,\n },\n artemis2: {\n name: \"Artemis II\",\n fn: artemis2,\n period: TWO_PI,\n speed: 0.7,\n },\n};\n","export type {\n Point,\n CurveDef,\n Engine,\n SarmalInstance,\n RendererOptions,\n SarmalOptions,\n} from \"./types\";\n\nexport { createEngine } from \"./engine\";\nexport { createRenderer } from \"./renderer\";\nexport { curves } from \"./curves\";\n\nimport type { CurveDef, SarmalInstance, SarmalOptions } from \"./types\";\nimport { createEngine } from \"./engine\";\nimport { createRenderer } from \"./renderer\";\n\n/**\n * Creates a sarmal animation on a canvas element\n *\n * @example\n * ```ts\n * import { createSarmal, curves } from 'sarmal'\n * const sarmal = createSarmal(canvas, curves.artemis2)\n * sarmal.start()\n * ```\n */\nexport function createSarmal(\n canvas: HTMLCanvasElement,\n curveDef: CurveDef,\n options?: SarmalOptions,\n): SarmalInstance {\n const { trailLength, ...rendererOpts } = options ?? {};\n const engine = createEngine(curveDef, trailLength);\n\n return createRenderer({ canvas, engine, ...rendererOpts });\n}\n"]}
@@ -0,0 +1,118 @@
1
+ interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ interface CurveDef {
6
+ name: string;
7
+ fn: (t: number, time: number, params: Record<string, number>) => Point;
8
+ /**
9
+ * @default (Math.PI * 2)
10
+ */
11
+ period?: number;
12
+ /**
13
+ * @default 1
14
+ */
15
+ speed?: number;
16
+ /**
17
+ * To indicate a compatible library version when providing the curve definitions
18
+ * Intended for potential backwards compatibility scenarios and futureproofing
19
+ */
20
+ version?: number;
21
+ }
22
+ interface Engine {
23
+ /**
24
+ * Advances the Sarmal simulation by the given delta time (dt) in seconds.
25
+ * Internally, this increases the simulation time `t` by `speed * dt`,
26
+ * wraps `t` at `period`, evaluates the curve's parametric function `fn(t)`,
27
+ * and appends the new point to the trail.
28
+ * Returns a `Point` array, which are sorted oldest to newest
29
+ * @param deltaTime Delta time in seconds (typically frame time from **requestAnimationFrame** or similar)
30
+ */
31
+ tick(deltaTime: number): Array<Point>;
32
+ /**
33
+ * Resets the simulation state, by clearing the trail and reverting internal time `t` to 0.
34
+ * The next call to `tick` will start fresh from the beginning of the curve.
35
+ */
36
+ reset(): void;
37
+ /**
38
+ * Returns the *skeleton* of the curve.
39
+ * In technicality, it just represents the complete traversal of the curve over one full period,
40
+ * which is sampled at points from `t=0` to `t=period`
41
+ *
42
+ * The skeleton is always derived from the curve's state at `t=0` using `fn(t, 0)`
43
+ * This represents the full path the sarmal would trace on its first complete cycle,
44
+ * rendered as a static background reference.
45
+ *
46
+ * The number of sample points is automatically derived from the curve's period.
47
+ */
48
+ getSarmalSkeleton(): Array<Point>;
49
+ }
50
+ interface SarmalInstance {
51
+ start(): void;
52
+ stop(): void;
53
+ /** Resets the engine and clears the trail */
54
+ reset(): void;
55
+ /** Stops the animation and cleans up resources */
56
+ destroy(): void;
57
+ }
58
+ interface RendererOptions {
59
+ /** Target canvas element that will contain the Sarmal */
60
+ canvas: HTMLCanvasElement;
61
+ engine: Engine;
62
+ /**
63
+ * @default '#ffffff'
64
+ */
65
+ skeletonColor?: string;
66
+ /**
67
+ * @default '#ffffff'
68
+ */
69
+ trailColor?: string;
70
+ /**
71
+ * @default '#ffffff'
72
+ */
73
+ headColor?: string;
74
+ /** @default 4 */
75
+ headRadius?: number;
76
+ /** @default 20 */
77
+ glowSize?: number;
78
+ }
79
+ interface SarmalOptions extends Omit<RendererOptions, "canvas" | "engine"> {
80
+ /** @default 120 */
81
+ trailLength?: number;
82
+ }
83
+
84
+ /**
85
+ * Creates the core simulation engine for a sarmal
86
+ *
87
+ * it runs a clock (time `t`), asks the curve for the current Point position at that time,
88
+ * and remembers the last N positions so the renderer can draw the trail
89
+ *
90
+ * The engine is only responsible for math coordinates,
91
+ * so it is not responsible for drawing or colors
92
+ *
93
+ * @param curveDef A curve definition
94
+ * @param trailLength default: `120`
95
+ */
96
+ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
97
+
98
+ /**
99
+ * Creates a Canvas 2D renderer for sarmal animations
100
+ * Renders the skeleton, the trail, and the glowing dot.
101
+ */
102
+ declare function createRenderer(options: RendererOptions): SarmalInstance;
103
+
104
+ declare const curves: Record<string, CurveDef>;
105
+
106
+ /**
107
+ * Creates a sarmal animation on a canvas element
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { createSarmal, curves } from 'sarmal'
112
+ * const sarmal = createSarmal(canvas, curves.artemis2)
113
+ * sarmal.start()
114
+ * ```
115
+ */
116
+ declare function createSarmal(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: SarmalOptions): SarmalInstance;
117
+
118
+ export { type CurveDef, type Engine, type Point, type RendererOptions, type SarmalInstance, type SarmalOptions, createEngine, createRenderer, createSarmal, curves };
@@ -0,0 +1,118 @@
1
+ interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ interface CurveDef {
6
+ name: string;
7
+ fn: (t: number, time: number, params: Record<string, number>) => Point;
8
+ /**
9
+ * @default (Math.PI * 2)
10
+ */
11
+ period?: number;
12
+ /**
13
+ * @default 1
14
+ */
15
+ speed?: number;
16
+ /**
17
+ * To indicate a compatible library version when providing the curve definitions
18
+ * Intended for potential backwards compatibility scenarios and futureproofing
19
+ */
20
+ version?: number;
21
+ }
22
+ interface Engine {
23
+ /**
24
+ * Advances the Sarmal simulation by the given delta time (dt) in seconds.
25
+ * Internally, this increases the simulation time `t` by `speed * dt`,
26
+ * wraps `t` at `period`, evaluates the curve's parametric function `fn(t)`,
27
+ * and appends the new point to the trail.
28
+ * Returns a `Point` array, which are sorted oldest to newest
29
+ * @param deltaTime Delta time in seconds (typically frame time from **requestAnimationFrame** or similar)
30
+ */
31
+ tick(deltaTime: number): Array<Point>;
32
+ /**
33
+ * Resets the simulation state, by clearing the trail and reverting internal time `t` to 0.
34
+ * The next call to `tick` will start fresh from the beginning of the curve.
35
+ */
36
+ reset(): void;
37
+ /**
38
+ * Returns the *skeleton* of the curve.
39
+ * In technicality, it just represents the complete traversal of the curve over one full period,
40
+ * which is sampled at points from `t=0` to `t=period`
41
+ *
42
+ * The skeleton is always derived from the curve's state at `t=0` using `fn(t, 0)`
43
+ * This represents the full path the sarmal would trace on its first complete cycle,
44
+ * rendered as a static background reference.
45
+ *
46
+ * The number of sample points is automatically derived from the curve's period.
47
+ */
48
+ getSarmalSkeleton(): Array<Point>;
49
+ }
50
+ interface SarmalInstance {
51
+ start(): void;
52
+ stop(): void;
53
+ /** Resets the engine and clears the trail */
54
+ reset(): void;
55
+ /** Stops the animation and cleans up resources */
56
+ destroy(): void;
57
+ }
58
+ interface RendererOptions {
59
+ /** Target canvas element that will contain the Sarmal */
60
+ canvas: HTMLCanvasElement;
61
+ engine: Engine;
62
+ /**
63
+ * @default '#ffffff'
64
+ */
65
+ skeletonColor?: string;
66
+ /**
67
+ * @default '#ffffff'
68
+ */
69
+ trailColor?: string;
70
+ /**
71
+ * @default '#ffffff'
72
+ */
73
+ headColor?: string;
74
+ /** @default 4 */
75
+ headRadius?: number;
76
+ /** @default 20 */
77
+ glowSize?: number;
78
+ }
79
+ interface SarmalOptions extends Omit<RendererOptions, "canvas" | "engine"> {
80
+ /** @default 120 */
81
+ trailLength?: number;
82
+ }
83
+
84
+ /**
85
+ * Creates the core simulation engine for a sarmal
86
+ *
87
+ * it runs a clock (time `t`), asks the curve for the current Point position at that time,
88
+ * and remembers the last N positions so the renderer can draw the trail
89
+ *
90
+ * The engine is only responsible for math coordinates,
91
+ * so it is not responsible for drawing or colors
92
+ *
93
+ * @param curveDef A curve definition
94
+ * @param trailLength default: `120`
95
+ */
96
+ declare function createEngine(curveDef: CurveDef, trailLength?: number): Engine;
97
+
98
+ /**
99
+ * Creates a Canvas 2D renderer for sarmal animations
100
+ * Renders the skeleton, the trail, and the glowing dot.
101
+ */
102
+ declare function createRenderer(options: RendererOptions): SarmalInstance;
103
+
104
+ declare const curves: Record<string, CurveDef>;
105
+
106
+ /**
107
+ * Creates a sarmal animation on a canvas element
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { createSarmal, curves } from 'sarmal'
112
+ * const sarmal = createSarmal(canvas, curves.artemis2)
113
+ * sarmal.start()
114
+ * ```
115
+ */
116
+ declare function createSarmal(canvas: HTMLCanvasElement, curveDef: CurveDef, options?: SarmalOptions): SarmalInstance;
117
+
118
+ export { type CurveDef, type Engine, type Point, type RendererOptions, type SarmalInstance, type SarmalOptions, createEngine, createRenderer, createSarmal, curves };