@sarmal/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +89 -3
- package/dist/auto-init.cjs +145 -111
- package/dist/auto-init.cjs.map +1 -1
- package/dist/auto-init.js +144 -110
- package/dist/auto-init.js.map +1 -1
- package/dist/index.cjs +181 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +180 -167
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
- package/dist/auto-init.d.cts +0 -2
- package/dist/auto-init.d.ts +0 -2
- package/dist/index.d.cts +0 -262
- package/dist/index.d.ts +0 -262
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Sarmal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sarmal/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>Parametric curve animations for loading/thinking indicators</strong>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://sarmal.art" target="_blank">Live Demo at sarmal.art</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**@sarmal/core** is a lightweight library for rendering elegant parametric curve animations.
|
|
14
|
+
|
|
15
|
+
The animations can be used anywhere you want. Use it for loading spinners, progress indicators, or to indicate that your very special AI model is _thinking_, up to you.
|
|
16
|
+
|
|
17
|
+
In web applications and perhaps also in the terminal maybe if possible!
|
|
18
|
+
|
|
19
|
+
- **Canvas & SVG renderers**: choose one or the other, but why not both?
|
|
20
|
+
- **standard curves**: default cliche curves any LLM can generate in seconds, from classic spirals to custom parametric paths
|
|
21
|
+
- **TIME CONTROL**: programmatic time stepping, seeking, and trail effects
|
|
22
|
+
- **Zero dependencies**: tiny bundle, quick to get started
|
|
23
|
+
- **TypeScript-first**: because who would build anyhing complex in pure JS?!
|
|
24
|
+
- full type safety, but no assurance it will work in runtime!
|
|
25
|
+
|
|
26
|
+
## Install
|
|
4
27
|
|
|
5
28
|
```bash
|
|
6
|
-
npm
|
|
29
|
+
npm install @sarmal/core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or use directly from CDN:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script type="module">
|
|
36
|
+
import { createSarmal, curves } from "https://cdn.jsdelivr.net/npm/@sarmal/core/+esm";
|
|
37
|
+
// your code here
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { createSarmal, curves } from "@sarmal/core";
|
|
45
|
+
|
|
46
|
+
const canvas = document.getElementById("my-canvas");
|
|
47
|
+
const sarmal = createSarmal(canvas, curves.artemis2, {
|
|
48
|
+
trailLength: 30,
|
|
49
|
+
strokeStyle: "#00ffaa",
|
|
50
|
+
lineWidth: 2,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
sarmal.start();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or with **auto-init** without having to write any JS:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script src="https://cdn.jsdelivr.net/npm/@sarmal/core/dist/auto-init.js"></script>
|
|
60
|
+
<canvas data-sarmal="artemis2" width="200" height="200"></canvas>
|
|
7
61
|
```
|
|
62
|
+
|
|
63
|
+
## Standard Curves
|
|
64
|
+
|
|
65
|
+
| Name | Description |
|
|
66
|
+
| -------------- | ------------------------------------------ |
|
|
67
|
+
| `artemis2` | Artemis II free-return lunar trajectory |
|
|
68
|
+
| `epitrochoid7` | 7-lobed epitrochoid with dynamic variation |
|
|
69
|
+
| `astroid` | 4-cusped astroid |
|
|
70
|
+
| `deltoid` | 3-cusped deltoid |
|
|
71
|
+
| `rose5` | 5-petal rose curve |
|
|
72
|
+
| `rose3` | 3-petal rose curve |
|
|
73
|
+
| `lissajous32` | Lissajous 3:2 with live skeleton |
|
|
74
|
+
| `lissajous43` | Lissajous 4:3 with live skeleton |
|
|
75
|
+
| `epicycloid3` | 3-cusped epicycloid |
|
|
76
|
+
| `lame` | Lamé curve with live skeleton |
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
Full API reference, examples, SVG renderer usage, engine time control (`seek`, `seekWithTrail`), custom curve definitions, and framework guides are available at [sarmal.art/docs](https://sarmal.art/docs)
|
|
81
|
+
|
|
82
|
+
## Inspiration
|
|
83
|
+
|
|
84
|
+
Inspired by [@bbssppllvv's tweet](https://x.com/bbssppllvv/status/2038718410318659763)
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT © [Alper Halil](https://aktasalper.com)
|
|
89
|
+
|
|
90
|
+
## Links
|
|
91
|
+
|
|
92
|
+
- [Homepage](https://sarmal.art): See all curves in action
|
|
93
|
+
- [npm](https://www.npmjs.com/package/@sarmal/core): Package registry
|
package/dist/auto-init.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// src/engine.ts
|
|
4
4
|
var TWO_PI = Math.PI * 2;
|
|
@@ -51,7 +51,7 @@ function resolveCurve(curveDef) {
|
|
|
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
56
|
}
|
|
57
57
|
function createEngine(curveDef, trailLength = 120) {
|
|
@@ -77,7 +77,7 @@ function createEngine(curveDef, trailLength = 120) {
|
|
|
77
77
|
actualTime += deltaTime;
|
|
78
78
|
if (morphCurveB !== null && _morphAlpha !== null) {
|
|
79
79
|
const a = curve.fn(t, actualTime, {});
|
|
80
|
-
const tB = _morphStrategy === "normalized" ? t / curve.period * morphCurveB.period : t;
|
|
80
|
+
const tB = _morphStrategy === "normalized" ? (t / curve.period) * morphCurveB.period : t;
|
|
81
81
|
const b = morphCurveB.fn(tB, actualTime, {});
|
|
82
82
|
trail.push(a.x + (b.x - a.x) * _morphAlpha, a.y + (b.y - a.y) * _morphAlpha);
|
|
83
83
|
} else {
|
|
@@ -101,14 +101,14 @@ function createEngine(curveDef, trailLength = 120) {
|
|
|
101
101
|
trail.clear();
|
|
102
102
|
},
|
|
103
103
|
seek(newT, { clearTrail = false } = {}) {
|
|
104
|
-
t = (newT % curve.period + curve.period) % curve.period;
|
|
104
|
+
t = ((newT % curve.period) + curve.period) % curve.period;
|
|
105
105
|
if (clearTrail) {
|
|
106
106
|
trail.clear();
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
seekWithTrail(targetT, { wrap = false, step = curve.period / trailLength } = {}) {
|
|
110
110
|
const advance = curve.speed * step;
|
|
111
|
-
const target = (targetT % curve.period + curve.period) % curve.period;
|
|
111
|
+
const target = ((targetT % curve.period) + curve.period) % curve.period;
|
|
112
112
|
const targetTime = target / curve.speed;
|
|
113
113
|
t = target;
|
|
114
114
|
actualTime = targetTime;
|
|
@@ -134,13 +134,16 @@ function createEngine(curveDef, trailLength = 120) {
|
|
|
134
134
|
...frozenB,
|
|
135
135
|
fn: (sampleT, time, params) => {
|
|
136
136
|
const a = frozenA.fn(sampleT, time, params);
|
|
137
|
-
const tB =
|
|
137
|
+
const tB =
|
|
138
|
+
frozenStrategy === "normalized"
|
|
139
|
+
? (sampleT / frozenA.period) * frozenB.period
|
|
140
|
+
: sampleT;
|
|
138
141
|
const b = frozenB.fn(tB, time, params);
|
|
139
142
|
return {
|
|
140
143
|
x: a.x + (b.x - a.x) * frozenAlpha,
|
|
141
|
-
y: a.y + (b.y - a.y) * frozenAlpha
|
|
144
|
+
y: a.y + (b.y - a.y) * frozenAlpha,
|
|
142
145
|
};
|
|
143
|
-
}
|
|
146
|
+
},
|
|
144
147
|
};
|
|
145
148
|
}
|
|
146
149
|
_morphStrategy = strategy;
|
|
@@ -153,7 +156,7 @@ function createEngine(curveDef, trailLength = 120) {
|
|
|
153
156
|
completeMorph() {
|
|
154
157
|
if (morphCurveB !== null) {
|
|
155
158
|
if (_morphStrategy === "normalized" && curve.period !== morphCurveB.period) {
|
|
156
|
-
t = t / curve.period * morphCurveB.period;
|
|
159
|
+
t = (t / curve.period) * morphCurveB.period;
|
|
157
160
|
}
|
|
158
161
|
curve = morphCurveB;
|
|
159
162
|
}
|
|
@@ -165,43 +168,74 @@ function createEngine(curveDef, trailLength = 120) {
|
|
|
165
168
|
const points = new Array(steps);
|
|
166
169
|
if (morphCurveB !== null && _morphAlpha !== null) {
|
|
167
170
|
for (let i = 0; i < steps; i++) {
|
|
168
|
-
const sampleT = i / (steps - 1) * curve.period;
|
|
171
|
+
const sampleT = (i / (steps - 1)) * curve.period;
|
|
169
172
|
const a = sampleSkeleton(curve, sampleT);
|
|
170
|
-
const tB =
|
|
173
|
+
const tB =
|
|
174
|
+
_morphStrategy === "normalized"
|
|
175
|
+
? (sampleT / curve.period) * morphCurveB.period
|
|
176
|
+
: sampleT;
|
|
171
177
|
const b = sampleSkeleton(morphCurveB, tB);
|
|
172
178
|
points[i] = {
|
|
173
179
|
x: a.x + (b.x - a.x) * _morphAlpha,
|
|
174
|
-
y: a.y + (b.y - a.y) * _morphAlpha
|
|
180
|
+
y: a.y + (b.y - a.y) * _morphAlpha,
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
return points;
|
|
178
184
|
}
|
|
179
185
|
for (let i = 0; i < steps; i++) {
|
|
180
|
-
const sampleT = i / (steps - 1) * curve.period;
|
|
186
|
+
const sampleT = (i / (steps - 1)) * curve.period;
|
|
181
187
|
points[i] = sampleSkeleton(curve, sampleT);
|
|
182
188
|
}
|
|
183
189
|
return points;
|
|
184
|
-
}
|
|
190
|
+
},
|
|
185
191
|
};
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
// src/renderer.ts
|
|
189
195
|
var DEFAULT_MORPH_DURATION_MS = 300;
|
|
190
196
|
var DEFAULT_HEAD_RADIUS = 4;
|
|
191
|
-
var DEFAULT_GLOW_SIZE = 20;
|
|
192
197
|
var DEFAULT_SKELETON_COLOR = "#ffffff";
|
|
193
198
|
var DEFAULT_SKELETON_OPACITY = 0.15;
|
|
194
199
|
var FIT_PADDING = 0.1;
|
|
195
|
-
var TRAIL_BATCH_SIZE = 20;
|
|
196
200
|
var TRAIL_FADE_CURVE = 1.5;
|
|
197
201
|
var TRAIL_MAX_OPACITY = 0.88;
|
|
198
202
|
var TRAIL_MIN_WIDTH = 0.5;
|
|
199
203
|
var TRAIL_MAX_WIDTH = 2.5;
|
|
200
|
-
var GLOW_INNER_EDGE = 0.4;
|
|
201
|
-
var GLOW_FALLOFF_OPACITY = 0.53;
|
|
202
204
|
function hexToRgbComponents(hex) {
|
|
203
205
|
const n = parseInt(hex.slice(1), 16);
|
|
204
|
-
return `${n >> 16},${n >> 8 & 255},${n & 255}`;
|
|
206
|
+
return `${n >> 16},${(n >> 8) & 255},${n & 255}`;
|
|
207
|
+
}
|
|
208
|
+
function computeTangent(trail, i) {
|
|
209
|
+
const count = trail.length;
|
|
210
|
+
if (count < 2) {
|
|
211
|
+
return { x: 1, y: 0 };
|
|
212
|
+
}
|
|
213
|
+
if (i === 0) {
|
|
214
|
+
const dx2 = trail[1].x - trail[0].x;
|
|
215
|
+
const dy2 = trail[1].y - trail[0].y;
|
|
216
|
+
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
|
|
217
|
+
return { x: dx2 / len2, y: dy2 / len2 };
|
|
218
|
+
}
|
|
219
|
+
if (i === count - 1) {
|
|
220
|
+
const dx2 = trail[count - 1].x - trail[count - 2].x;
|
|
221
|
+
const dy2 = trail[count - 1].y - trail[count - 2].y;
|
|
222
|
+
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
|
|
223
|
+
return { x: dx2 / len2, y: dy2 / len2 };
|
|
224
|
+
}
|
|
225
|
+
const dx = trail[i + 1].x - trail[i - 1].x;
|
|
226
|
+
const dy = trail[i + 1].y - trail[i - 1].y;
|
|
227
|
+
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
228
|
+
return { x: dx / len, y: dy / len };
|
|
229
|
+
}
|
|
230
|
+
function computeNormal(trail, i) {
|
|
231
|
+
const tangent = computeTangent(trail, i);
|
|
232
|
+
return { x: -tangent.y, y: tangent.x };
|
|
233
|
+
}
|
|
234
|
+
function applyDprSizing(target, logicalWidth, logicalHeight, dpr) {
|
|
235
|
+
target.style.width = `${logicalWidth}px`;
|
|
236
|
+
target.style.height = `${logicalHeight}px`;
|
|
237
|
+
target.width = logicalWidth * dpr;
|
|
238
|
+
target.height = logicalHeight * dpr;
|
|
205
239
|
}
|
|
206
240
|
function createRenderer(options) {
|
|
207
241
|
const canvas = options.canvas;
|
|
@@ -215,10 +249,19 @@ function createRenderer(options) {
|
|
|
215
249
|
trailColor: options.trailColor ?? "#ffffff",
|
|
216
250
|
headColor: options.headColor ?? "#ffffff",
|
|
217
251
|
headRadius: options.headRadius ?? DEFAULT_HEAD_RADIUS,
|
|
218
|
-
glowSize: options.glowSize ?? DEFAULT_GLOW_SIZE
|
|
219
252
|
};
|
|
220
253
|
const trailRgb = hexToRgbComponents(opts.trailColor);
|
|
221
|
-
const
|
|
254
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
255
|
+
function setupCanvas() {
|
|
256
|
+
const rect = canvas.getBoundingClientRect();
|
|
257
|
+
const lw = rect.width || 200;
|
|
258
|
+
const lh = rect.height || 200;
|
|
259
|
+
applyDprSizing(canvas, lw, lh, dpr);
|
|
260
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
261
|
+
}
|
|
262
|
+
setupCanvas();
|
|
263
|
+
let logicalWidth = canvas.width / dpr;
|
|
264
|
+
let logicalHeight = canvas.height / dpr;
|
|
222
265
|
let skeleton = [];
|
|
223
266
|
let skeletonCanvas = null;
|
|
224
267
|
let trail = [];
|
|
@@ -232,12 +275,13 @@ function createRenderer(options) {
|
|
|
232
275
|
let morphResolve = null;
|
|
233
276
|
let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
|
|
234
277
|
let morphAlpha = 0;
|
|
235
|
-
let morphBoundsA = null;
|
|
236
|
-
let morphBoundsB = null;
|
|
237
278
|
function computeBoundaries(pts) {
|
|
238
279
|
if (pts.length === 0) return null;
|
|
239
280
|
const first = pts[0];
|
|
240
|
-
let minX = first.x,
|
|
281
|
+
let minX = first.x,
|
|
282
|
+
maxX = first.x,
|
|
283
|
+
minY = first.y,
|
|
284
|
+
maxY = first.y;
|
|
241
285
|
for (const p of pts) {
|
|
242
286
|
if (p.x < minX) minX = p.x;
|
|
243
287
|
if (p.x > maxX) maxX = p.x;
|
|
@@ -246,17 +290,15 @@ function createRenderer(options) {
|
|
|
246
290
|
}
|
|
247
291
|
const width = maxX - minX;
|
|
248
292
|
const height = maxY - minY;
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const scaleX = canvasWidth / (width * (1 + FIT_PADDING * 2));
|
|
252
|
-
const scaleY = canvasHeight / (height * (1 + FIT_PADDING * 2));
|
|
293
|
+
const scaleX = logicalWidth / (width * (1 + FIT_PADDING * 2));
|
|
294
|
+
const scaleY = logicalHeight / (height * (1 + FIT_PADDING * 2));
|
|
253
295
|
const s = Math.min(scaleX, scaleY);
|
|
254
296
|
const boundsWidth = width * s;
|
|
255
297
|
const boundsHeight = height * s;
|
|
256
298
|
return {
|
|
257
299
|
scale: s,
|
|
258
|
-
offsetX: (
|
|
259
|
-
offsetY: (
|
|
300
|
+
offsetX: (logicalWidth - boundsWidth) / 2 - minX * s,
|
|
301
|
+
offsetY: (logicalHeight - boundsHeight) / 2 - minY * s,
|
|
260
302
|
};
|
|
261
303
|
}
|
|
262
304
|
function calculateBoundaries() {
|
|
@@ -271,6 +313,7 @@ function createRenderer(options) {
|
|
|
271
313
|
if (skeleton.length < 2) return;
|
|
272
314
|
skeletonCanvas = new OffscreenCanvas(canvas.width, canvas.height);
|
|
273
315
|
const skeletonCtx = skeletonCanvas.getContext("2d");
|
|
316
|
+
skeletonCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
274
317
|
skeletonCtx.strokeStyle = `rgba(${hexToRgbComponents(opts.skeletonColor)},${DEFAULT_SKELETON_OPACITY})`;
|
|
275
318
|
skeletonCtx.lineWidth = 1.5;
|
|
276
319
|
skeletonCtx.beginPath();
|
|
@@ -316,32 +359,41 @@ function createRenderer(options) {
|
|
|
316
359
|
}
|
|
317
360
|
ctx.stroke();
|
|
318
361
|
} else if (skeletonCanvas) {
|
|
319
|
-
ctx.drawImage(skeletonCanvas, 0, 0);
|
|
362
|
+
ctx.drawImage(skeletonCanvas, 0, 0, logicalWidth, logicalHeight);
|
|
320
363
|
}
|
|
321
364
|
}
|
|
322
365
|
function drawTrail() {
|
|
323
366
|
if (trailCount < 2) {
|
|
324
367
|
return;
|
|
325
368
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const bEnd = Math.min(batchIndex + TRAIL_BATCH_SIZE, trailCount - 1);
|
|
330
|
-
const progress = (batchIndex + bEnd) / 2 / (trailCount - 1);
|
|
369
|
+
for (let i = 0; i < trailCount - 1; i++) {
|
|
370
|
+
const progress = i / (trailCount - 1);
|
|
371
|
+
const nextProgress = (i + 1) / (trailCount - 1);
|
|
331
372
|
const alpha = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
|
|
332
|
-
const
|
|
373
|
+
const width = TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
|
|
374
|
+
const nextWidth = TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH);
|
|
375
|
+
const curr = trail[i];
|
|
376
|
+
const next = trail[i + 1];
|
|
377
|
+
const n0 = computeNormal(trail, i);
|
|
378
|
+
const n1 = computeNormal(trail, i + 1);
|
|
379
|
+
const halfW0 = width / 2;
|
|
380
|
+
const halfW1 = nextWidth / 2;
|
|
381
|
+
const l0x = curr.x * scale + offsetX + n0.x * halfW0;
|
|
382
|
+
const l0y = curr.y * scale + offsetY + n0.y * halfW0;
|
|
383
|
+
const r0x = curr.x * scale + offsetX - n0.x * halfW0;
|
|
384
|
+
const r0y = curr.y * scale + offsetY - n0.y * halfW0;
|
|
385
|
+
const l1x = next.x * scale + offsetX + n1.x * halfW1;
|
|
386
|
+
const l1y = next.y * scale + offsetY + n1.y * halfW1;
|
|
387
|
+
const r1x = next.x * scale + offsetX - n1.x * halfW1;
|
|
388
|
+
const r1y = next.y * scale + offsetY - n1.y * halfW1;
|
|
389
|
+
ctx.fillStyle = `rgba(${trailRgb},${alpha})`;
|
|
333
390
|
ctx.beginPath();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
ctx.strokeStyle = `rgba(${trailRgb},${alpha})`;
|
|
343
|
-
ctx.lineWidth = lineWidth;
|
|
344
|
-
ctx.stroke();
|
|
391
|
+
ctx.moveTo(l0x, l0y);
|
|
392
|
+
ctx.lineTo(l1x, l1y);
|
|
393
|
+
ctx.lineTo(r1x, r1y);
|
|
394
|
+
ctx.lineTo(r0x, r0y);
|
|
395
|
+
ctx.closePath();
|
|
396
|
+
ctx.fill();
|
|
345
397
|
}
|
|
346
398
|
}
|
|
347
399
|
function drawHead() {
|
|
@@ -350,14 +402,6 @@ function createRenderer(options) {
|
|
|
350
402
|
}
|
|
351
403
|
const x = head.x * scale + offsetX;
|
|
352
404
|
const y = head.y * scale + offsetY;
|
|
353
|
-
const gradient = ctx.createRadialGradient(x, y, 0, x, y, opts.glowSize);
|
|
354
|
-
gradient.addColorStop(0, opts.headColor);
|
|
355
|
-
gradient.addColorStop(GLOW_INNER_EDGE, headRgbFalloff);
|
|
356
|
-
gradient.addColorStop(1, "transparent");
|
|
357
|
-
ctx.fillStyle = gradient;
|
|
358
|
-
ctx.beginPath();
|
|
359
|
-
ctx.arc(x, y, opts.glowSize, 0, Math.PI * 2);
|
|
360
|
-
ctx.fill();
|
|
361
405
|
ctx.fillStyle = opts.headColor;
|
|
362
406
|
ctx.beginPath();
|
|
363
407
|
ctx.arc(x, y, opts.headRadius, 0, Math.PI * 2);
|
|
@@ -370,20 +414,18 @@ function createRenderer(options) {
|
|
|
370
414
|
if (engine.morphAlpha !== null) {
|
|
371
415
|
morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
|
|
372
416
|
engine.setMorphAlpha(morphAlpha);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
scale =
|
|
377
|
-
offsetX =
|
|
378
|
-
offsetY =
|
|
417
|
+
const interpolatedSkeleton = engine.getSarmalSkeleton();
|
|
418
|
+
const bounds = computeBoundaries(interpolatedSkeleton);
|
|
419
|
+
if (bounds) {
|
|
420
|
+
scale = bounds.scale;
|
|
421
|
+
offsetX = bounds.offsetX;
|
|
422
|
+
offsetY = bounds.offsetY;
|
|
379
423
|
}
|
|
380
424
|
if (morphAlpha >= 1) {
|
|
381
425
|
engine.completeMorph();
|
|
382
426
|
morphResolve?.();
|
|
383
427
|
morphResolve = null;
|
|
384
428
|
morphAlpha = 0;
|
|
385
|
-
morphBoundsA = null;
|
|
386
|
-
morphBoundsB = null;
|
|
387
429
|
skeleton = engine.getSarmalSkeleton();
|
|
388
430
|
if (!engine.isLiveSkeleton) {
|
|
389
431
|
buildSkeletonCanvas();
|
|
@@ -393,7 +435,7 @@ function createRenderer(options) {
|
|
|
393
435
|
trail = engine.tick(deltaTime);
|
|
394
436
|
trailCount = engine.trailCount;
|
|
395
437
|
head = trailCount > 0 ? trail[trailCount - 1] : null;
|
|
396
|
-
ctx.clearRect(0, 0,
|
|
438
|
+
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
|
|
397
439
|
if (engine.isLiveSkeleton && engine.morphAlpha === null) {
|
|
398
440
|
skeleton = engine.getSarmalSkeleton();
|
|
399
441
|
calculateBoundaries();
|
|
@@ -441,57 +483,48 @@ function createRenderer(options) {
|
|
|
441
483
|
engine.seekWithTrail(t);
|
|
442
484
|
},
|
|
443
485
|
morphTo(target, options2) {
|
|
444
|
-
const interruptBounds = morphResolve !== null ? { scale, offsetX, offsetY } : null;
|
|
445
486
|
if (morphResolve !== null) {
|
|
446
487
|
engine.completeMorph();
|
|
447
488
|
morphResolve();
|
|
448
489
|
morphResolve = null;
|
|
449
490
|
morphAlpha = 0;
|
|
450
|
-
morphBoundsA = null;
|
|
451
|
-
morphBoundsB = null;
|
|
452
491
|
}
|
|
453
492
|
morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
|
|
454
493
|
morphAlpha = 0;
|
|
455
|
-
morphBoundsA = interruptBounds ?? computeBoundaries(engine.getSarmalSkeleton()) ?? { scale, offsetX, offsetY };
|
|
456
494
|
engine.startMorph(target, options2?.morphStrategy);
|
|
457
|
-
const period = target.period ?? Math.PI * 2;
|
|
458
|
-
const samples = Math.max(50, Math.round(period * 20));
|
|
459
|
-
const skeletonFn = target.skeletonFn ?? ((t) => target.fn(t, 0, {}));
|
|
460
|
-
const skeletonB = Array.from(
|
|
461
|
-
{ length: samples + 1 },
|
|
462
|
-
(_, i) => skeletonFn(i / samples * period)
|
|
463
|
-
);
|
|
464
|
-
morphBoundsB = computeBoundaries(skeletonB) ?? { scale, offsetX, offsetY };
|
|
465
495
|
return new Promise((resolve) => {
|
|
466
496
|
morphResolve = resolve;
|
|
467
497
|
});
|
|
468
|
-
}
|
|
498
|
+
},
|
|
469
499
|
};
|
|
470
500
|
}
|
|
471
501
|
|
|
472
502
|
// src/curves.ts
|
|
473
503
|
var TWO_PI2 = Math.PI * 2;
|
|
474
504
|
function artemis2(t, _time, _params) {
|
|
475
|
-
const a = 0.35,
|
|
476
|
-
|
|
505
|
+
const a = 0.35,
|
|
506
|
+
b = 0.15,
|
|
507
|
+
ox = 0.175;
|
|
508
|
+
const s = Math.sin(t),
|
|
509
|
+
c = Math.cos(t);
|
|
477
510
|
const denom = 1 + s * s;
|
|
478
511
|
return {
|
|
479
|
-
x: c * (1 + a * c) / denom - ox,
|
|
480
|
-
y: s * c * (1 + b * c) / denom
|
|
512
|
+
x: (c * (1 + a * c)) / denom - ox,
|
|
513
|
+
y: (s * c * (1 + b * c)) / denom,
|
|
481
514
|
};
|
|
482
515
|
}
|
|
483
516
|
function epitrochoid7(t, _time, _params) {
|
|
484
517
|
const d = 1 + 0.55 * Math.sin(t * 0.5);
|
|
485
518
|
return {
|
|
486
519
|
x: 7 * Math.cos(t) - d * Math.cos(7 * t),
|
|
487
|
-
y: 7 * Math.sin(t) - d * Math.sin(7 * t)
|
|
520
|
+
y: 7 * Math.sin(t) - d * Math.sin(7 * t),
|
|
488
521
|
};
|
|
489
522
|
}
|
|
490
523
|
function epitrochoid7Skeleton(t) {
|
|
491
524
|
const d = 1.275;
|
|
492
525
|
return {
|
|
493
526
|
x: 7 * Math.cos(t) - d * Math.cos(7 * t),
|
|
494
|
-
y: 7 * Math.sin(t) - d * Math.sin(7 * t)
|
|
527
|
+
y: 7 * Math.sin(t) - d * Math.sin(7 * t),
|
|
495
528
|
};
|
|
496
529
|
}
|
|
497
530
|
function astroid(t, _time, _params) {
|
|
@@ -499,55 +532,56 @@ function astroid(t, _time, _params) {
|
|
|
499
532
|
const s = Math.sin(t);
|
|
500
533
|
return {
|
|
501
534
|
x: c * c * c,
|
|
502
|
-
y: s * s * s
|
|
535
|
+
y: s * s * s,
|
|
503
536
|
};
|
|
504
537
|
}
|
|
505
538
|
function deltoid(t, _time, _params) {
|
|
506
539
|
return {
|
|
507
540
|
x: 2 * Math.cos(t) + Math.cos(2 * t),
|
|
508
|
-
y: 2 * Math.sin(t) - Math.sin(2 * t)
|
|
541
|
+
y: 2 * Math.sin(t) - Math.sin(2 * t),
|
|
509
542
|
};
|
|
510
543
|
}
|
|
511
544
|
function rose5(t, _time, _params) {
|
|
512
545
|
const r = Math.cos(5 * t);
|
|
513
546
|
return {
|
|
514
547
|
x: r * Math.cos(t),
|
|
515
|
-
y: r * Math.sin(t)
|
|
548
|
+
y: r * Math.sin(t),
|
|
516
549
|
};
|
|
517
550
|
}
|
|
518
551
|
function rose3(t, _time, _params) {
|
|
519
552
|
const r = Math.cos(3 * t);
|
|
520
553
|
return {
|
|
521
554
|
x: r * Math.cos(t),
|
|
522
|
-
y: r * Math.sin(t)
|
|
555
|
+
y: r * Math.sin(t),
|
|
523
556
|
};
|
|
524
557
|
}
|
|
525
558
|
function lissajous32(t, time, _params) {
|
|
526
559
|
const phi = time * 0.45;
|
|
527
560
|
return {
|
|
528
561
|
x: Math.sin(3 * t + phi),
|
|
529
|
-
y: Math.sin(2 * t)
|
|
562
|
+
y: Math.sin(2 * t),
|
|
530
563
|
};
|
|
531
564
|
}
|
|
532
565
|
function lissajous43(t, time, _params) {
|
|
533
566
|
const phi = time * 0.38;
|
|
534
567
|
return {
|
|
535
568
|
x: Math.sin(4 * t + phi),
|
|
536
|
-
y: Math.sin(3 * t)
|
|
569
|
+
y: Math.sin(3 * t),
|
|
537
570
|
};
|
|
538
571
|
}
|
|
539
572
|
function epicycloid3(t, _time, _params) {
|
|
540
573
|
return {
|
|
541
574
|
x: 4 * Math.cos(t) - Math.cos(4 * t),
|
|
542
|
-
y: 4 * Math.sin(t) - Math.sin(4 * t)
|
|
575
|
+
y: 4 * Math.sin(t) - Math.sin(4 * t),
|
|
543
576
|
};
|
|
544
577
|
}
|
|
545
578
|
function lame(t, time, _params) {
|
|
546
579
|
const p = 1.75 + 1.25 * Math.sin(time * 0.48);
|
|
547
|
-
const c = Math.cos(t),
|
|
580
|
+
const c = Math.cos(t),
|
|
581
|
+
s = Math.sin(t);
|
|
548
582
|
return {
|
|
549
583
|
x: Math.sign(c) * Math.pow(Math.abs(c), p),
|
|
550
|
-
y: Math.sign(s) * Math.pow(Math.abs(s), p)
|
|
584
|
+
y: Math.sign(s) * Math.pow(Math.abs(s), p),
|
|
551
585
|
};
|
|
552
586
|
}
|
|
553
587
|
var curves = {
|
|
@@ -555,66 +589,66 @@ var curves = {
|
|
|
555
589
|
name: "Artemis II",
|
|
556
590
|
fn: artemis2,
|
|
557
591
|
period: TWO_PI2,
|
|
558
|
-
speed: 0.7
|
|
592
|
+
speed: 0.7,
|
|
559
593
|
},
|
|
560
594
|
epitrochoid7: {
|
|
561
595
|
name: "Epitrochoid",
|
|
562
596
|
fn: epitrochoid7,
|
|
563
597
|
period: TWO_PI2,
|
|
564
598
|
speed: 1.4,
|
|
565
|
-
skeletonFn: epitrochoid7Skeleton
|
|
599
|
+
skeletonFn: epitrochoid7Skeleton,
|
|
566
600
|
},
|
|
567
601
|
astroid: {
|
|
568
602
|
name: "Astroid",
|
|
569
603
|
fn: astroid,
|
|
570
604
|
period: TWO_PI2,
|
|
571
|
-
speed: 1.1
|
|
605
|
+
speed: 1.1,
|
|
572
606
|
},
|
|
573
607
|
deltoid: {
|
|
574
608
|
name: "Deltoid",
|
|
575
609
|
fn: deltoid,
|
|
576
610
|
period: TWO_PI2,
|
|
577
|
-
speed: 0.9
|
|
611
|
+
speed: 0.9,
|
|
578
612
|
},
|
|
579
613
|
rose5: {
|
|
580
614
|
name: "Rose (n=5)",
|
|
581
615
|
fn: rose5,
|
|
582
616
|
period: TWO_PI2,
|
|
583
|
-
speed: 1
|
|
617
|
+
speed: 1,
|
|
584
618
|
},
|
|
585
619
|
rose3: {
|
|
586
620
|
name: "Rose (n=3)",
|
|
587
621
|
fn: rose3,
|
|
588
622
|
period: TWO_PI2,
|
|
589
|
-
speed: 1.15
|
|
623
|
+
speed: 1.15,
|
|
590
624
|
},
|
|
591
625
|
lissajous32: {
|
|
592
626
|
name: "Lissajous 3:2",
|
|
593
627
|
fn: lissajous32,
|
|
594
628
|
period: TWO_PI2,
|
|
595
629
|
speed: 2,
|
|
596
|
-
skeleton: "live"
|
|
630
|
+
skeleton: "live",
|
|
597
631
|
},
|
|
598
632
|
lissajous43: {
|
|
599
633
|
name: "Lissajous 4:3",
|
|
600
634
|
fn: lissajous43,
|
|
601
635
|
period: TWO_PI2,
|
|
602
636
|
speed: 1.8,
|
|
603
|
-
skeleton: "live"
|
|
637
|
+
skeleton: "live",
|
|
604
638
|
},
|
|
605
639
|
epicycloid3: {
|
|
606
640
|
name: "Epicycloid (n=3)",
|
|
607
641
|
fn: epicycloid3,
|
|
608
642
|
period: TWO_PI2,
|
|
609
|
-
speed: 0.75
|
|
643
|
+
speed: 0.75,
|
|
610
644
|
},
|
|
611
645
|
lame: {
|
|
612
646
|
name: "Lam\xE9 Curve",
|
|
613
647
|
fn: lame,
|
|
614
648
|
period: TWO_PI2,
|
|
615
649
|
speed: 1,
|
|
616
|
-
skeleton: "live"
|
|
617
|
-
}
|
|
650
|
+
skeleton: "live",
|
|
651
|
+
},
|
|
618
652
|
};
|
|
619
653
|
|
|
620
654
|
// src/index.ts
|
|
@@ -637,12 +671,12 @@ function init() {
|
|
|
637
671
|
return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
|
|
638
672
|
}
|
|
639
673
|
const sarmal = createSarmal(canvas, curveDef, {
|
|
640
|
-
...canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor },
|
|
641
|
-
...canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor },
|
|
642
|
-
...canvas.dataset.headColor && { headColor: canvas.dataset.headColor },
|
|
643
|
-
...canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) },
|
|
644
|
-
...canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) },
|
|
645
|
-
...canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }
|
|
674
|
+
...(canvas.dataset.trailColor && { trailColor: canvas.dataset.trailColor }),
|
|
675
|
+
...(canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor }),
|
|
676
|
+
...(canvas.dataset.headColor && { headColor: canvas.dataset.headColor }),
|
|
677
|
+
...(canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) }),
|
|
678
|
+
...(canvas.dataset.glowSize && { glowSize: parseInt(canvas.dataset.glowSize, 10) }),
|
|
679
|
+
...(canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }),
|
|
646
680
|
});
|
|
647
681
|
sarmal.start();
|
|
648
682
|
});
|
|
@@ -653,4 +687,4 @@ if (document.readyState === "loading") {
|
|
|
653
687
|
init();
|
|
654
688
|
}
|
|
655
689
|
//# sourceMappingURL=auto-init.cjs.map
|
|
656
|
-
//# sourceMappingURL=auto-init.cjs.map
|
|
690
|
+
//# sourceMappingURL=auto-init.cjs.map
|