@sarmal/core 0.18.0 → 0.20.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
@@ -298,12 +298,20 @@ function computeNormal(trail, i) {
298
298
  const tangent = computeTangent(trail, i);
299
299
  return { x: -tangent.y, y: tangent.x };
300
300
  }
301
- function computeTrailQuad(trail, i, trailCount, toX, toY) {
301
+ function computeTrailQuad(
302
+ trail,
303
+ i,
304
+ trailCount,
305
+ toX,
306
+ toY,
307
+ minWidth = TRAIL_MIN_WIDTH,
308
+ maxWidth = TRAIL_MAX_WIDTH,
309
+ ) {
302
310
  const progress = i / (trailCount - 1);
303
311
  const nextProgress = (i + 1) / (trailCount - 1);
304
312
  const opacity = Math.pow(progress, TRAIL_FADE_CURVE) * TRAIL_MAX_OPACITY;
305
- const w0 = (TRAIL_MIN_WIDTH + progress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
306
- const w1 = (TRAIL_MIN_WIDTH + nextProgress * (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH)) / 2;
313
+ const w0 = (minWidth + progress * (maxWidth - minWidth)) / 2;
314
+ const w1 = (minWidth + nextProgress * (maxWidth - minWidth)) / 2;
307
315
  const curr = trail[i];
308
316
  const next = trail[i + 1];
309
317
  const n0 = computeNormal(trail, i);
@@ -814,6 +822,350 @@ function createRenderer(options) {
814
822
  return instance;
815
823
  }
816
824
 
825
+ // src/renderer-svg.ts
826
+ var EMPTY_PARAMS2 = {};
827
+ var HIGH_TRAIL_LENGTH_THRESHOLD = 5e3;
828
+ function pointsToPathString(pts, scale, offsetX, offsetY) {
829
+ if (pts.length < 2) {
830
+ return "";
831
+ }
832
+ const px = (p) => (p.x * scale + offsetX).toFixed(2);
833
+ const py = (p) => (p.y * scale + offsetY).toFixed(2);
834
+ let d = `M${px(pts[0])} ${py(pts[0])}`;
835
+ for (let i = 1; i < pts.length; i++) {
836
+ d += ` L${px(pts[i])} ${py(pts[i])}`;
837
+ }
838
+ return d + " Z";
839
+ }
840
+ function sampleCurveSkeleton(curveDef) {
841
+ const period = curveDef.period ?? Math.PI * 2;
842
+ const samples = Math.ceil(period * 50);
843
+ const pts = Array.from({ length: samples });
844
+ for (let i = 0; i < samples; i++) {
845
+ const t = (i / (samples - 1)) * period;
846
+ pts[i] = curveDef.skeletonFn ? curveDef.skeletonFn(t) : curveDef.fn(t, 0, EMPTY_PARAMS2);
847
+ }
848
+ return pts;
849
+ }
850
+ function el(tag) {
851
+ return document.createElementNS("http://www.w3.org/2000/svg", tag);
852
+ }
853
+ function createSVGRenderer(options) {
854
+ const { container, engine } = options;
855
+ const poolSize = engine.trailLength;
856
+ if (poolSize > HIGH_TRAIL_LENGTH_THRESHOLD) {
857
+ console.warn(
858
+ `[sarmal] High trailLength in SVG renderer (${poolSize}). Consider using the canvas renderer for long trails.`,
859
+ );
860
+ }
861
+ let trailStyle = options.trailStyle ?? "default";
862
+ let trailColor = options.trailColor ?? "#ffffff";
863
+ let skeletonColor = options.skeletonColor ?? "#ffffff";
864
+ let userHeadColor = options.headColor ?? null;
865
+ let headColor = userHeadColor ?? resolveHeadColor(trailColor, trailStyle);
866
+ let trailSolid = resolveTrailMainColor(trailColor);
867
+ let trailPalette = resolveTrailPalette(trailColor);
868
+ const ariaLabel = options.ariaLabel ?? "Loading";
869
+ warnIfTrailColorMismatch(trailColor, trailStyle);
870
+ const viewSize = 100;
871
+ const headRadius = options.headRadius ?? 1.5;
872
+ const svgTrailMinWidth = 0.25;
873
+ const svgTrailMaxWidth = 1.25;
874
+ const svgSkeletonStrokeWidth = "0.75";
875
+ container.setAttribute("viewBox", `0 0 ${viewSize} ${viewSize}`);
876
+ container.setAttribute("role", "img");
877
+ container.setAttribute("aria-label", ariaLabel);
878
+ const group = el("g");
879
+ const titleEl = el("title");
880
+ titleEl.textContent = ariaLabel;
881
+ group.appendChild(titleEl);
882
+ const skeletonPath = el("path");
883
+ skeletonPath.setAttribute("data-sarmal-role", "skeleton");
884
+ skeletonPath.setAttribute("fill", "none");
885
+ skeletonPath.setAttribute("stroke", skeletonColor);
886
+ skeletonPath.setAttribute("stroke-opacity", String(DEFAULT_SKELETON_OPACITY));
887
+ skeletonPath.setAttribute("stroke-width", svgSkeletonStrokeWidth);
888
+ if (skeletonColor === "transparent") {
889
+ skeletonPath.setAttribute("visibility", "hidden");
890
+ }
891
+ group.appendChild(skeletonPath);
892
+ const skeletonPathA = el("path");
893
+ skeletonPathA.setAttribute("fill", "none");
894
+ skeletonPathA.setAttribute("stroke", skeletonColor);
895
+ skeletonPathA.setAttribute("stroke-width", svgSkeletonStrokeWidth);
896
+ skeletonPathA.setAttribute("visibility", "hidden");
897
+ group.appendChild(skeletonPathA);
898
+ const skeletonPathB = el("path");
899
+ skeletonPathB.setAttribute("fill", "none");
900
+ skeletonPathB.setAttribute("stroke", skeletonColor);
901
+ skeletonPathB.setAttribute("stroke-width", svgSkeletonStrokeWidth);
902
+ skeletonPathB.setAttribute("visibility", "hidden");
903
+ group.appendChild(skeletonPathB);
904
+ let morphPathABuilt = "";
905
+ let morphPathBBuilt = "";
906
+ const trailPaths = [];
907
+ for (let i = 0; i < poolSize; i++) {
908
+ const path = el("path");
909
+ path.setAttribute("fill", trailSolid);
910
+ group.appendChild(path);
911
+ trailPaths.push(path);
912
+ }
913
+ const headCircle = el("circle");
914
+ headCircle.setAttribute("data-sarmal-role", "head");
915
+ headCircle.setAttribute("fill", headColor);
916
+ headCircle.setAttribute("r", String(headRadius));
917
+ group.appendChild(headCircle);
918
+ container.appendChild(group);
919
+ let gradientAnimTime = 0;
920
+ let scale = 1;
921
+ let offsetX = 0;
922
+ let offsetY = 0;
923
+ function applyBoundaries(skeleton2) {
924
+ const b = computeBoundaries(skeleton2, viewSize, viewSize);
925
+ if (b) {
926
+ scale = b.scale;
927
+ offsetX = b.offsetX;
928
+ offsetY = b.offsetY;
929
+ }
930
+ }
931
+ function px(p) {
932
+ return p.x * scale + offsetX;
933
+ }
934
+ function py(p) {
935
+ return p.y * scale + offsetY;
936
+ }
937
+ function updateSkeleton(skeleton2) {
938
+ skeletonPath.setAttribute("d", pointsToPathString(skeleton2, scale, offsetX, offsetY));
939
+ }
940
+ const skeleton = engine.getSarmalSkeleton();
941
+ applyBoundaries(skeleton);
942
+ if (!engine.isLiveSkeleton) {
943
+ updateSkeleton(skeleton);
944
+ }
945
+ function updateTrail(trail, trailCount) {
946
+ if (trailCount < 2) {
947
+ for (const p of trailPaths) {
948
+ p.setAttribute("d", "");
949
+ }
950
+ return;
951
+ }
952
+ const drawnCount = trailCount - 1;
953
+ for (let i = 0; i < drawnCount; i++) {
954
+ const { l0x, l0y, r0x, r0y, l1x, l1y, r1x, r1y, opacity, progress } = computeTrailQuad(
955
+ trail,
956
+ i,
957
+ trailCount,
958
+ px,
959
+ py,
960
+ svgTrailMinWidth,
961
+ svgTrailMaxWidth,
962
+ );
963
+ const d = `M${l0x.toFixed(2)} ${l0y.toFixed(2)} L${l1x.toFixed(2)} ${l1y.toFixed(2)} L${r1x.toFixed(2)} ${r1y.toFixed(2)} L${r0x.toFixed(2)} ${r0y.toFixed(2)} Z`;
964
+ trailPaths[i].setAttribute("d", d);
965
+ trailPaths[i].setAttribute("fill-opacity", opacity.toFixed(3));
966
+ if (trailStyle !== "default") {
967
+ const timeOffset = trailStyle === "gradient-animated" ? gradientAnimTime * 5e-4 : 0;
968
+ const { r, g, b } = getPaletteColor(trailPalette, progress, timeOffset);
969
+ trailPaths[i].setAttribute("fill", `rgb(${r},${g},${b})`);
970
+ }
971
+ }
972
+ for (let i = drawnCount; i < trailPaths.length; i++) {
973
+ trailPaths[i].setAttribute("d", "");
974
+ }
975
+ }
976
+ function updateHead(trail, trailCount) {
977
+ if (trailCount === 0) {
978
+ return;
979
+ }
980
+ const head = trail[trailCount - 1];
981
+ const x = px(head);
982
+ const y = py(head);
983
+ headCircle.setAttribute("cx", String(x));
984
+ headCircle.setAttribute("cy", String(y));
985
+ }
986
+ let animationId = null;
987
+ let lastTime = 0;
988
+ const prefersReducedMotion =
989
+ typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
990
+ let morphResolve = null;
991
+ let morphReject = null;
992
+ let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
993
+ let morphTarget = null;
994
+ let morphAlpha = 0;
995
+ function renderFrame(deltaTime) {
996
+ if (trailStyle === "gradient-animated") {
997
+ gradientAnimTime += deltaTime * 1e3;
998
+ }
999
+ if (engine.morphAlpha !== null) {
1000
+ morphAlpha = Math.min(1, morphAlpha + deltaTime / (morphDurationMs / 1e3));
1001
+ engine.setMorphAlpha(morphAlpha);
1002
+ if (morphPathABuilt) {
1003
+ skeletonPathA.setAttribute("d", morphPathABuilt);
1004
+ skeletonPathA.setAttribute("visibility", "visible");
1005
+ skeletonPathA.setAttribute(
1006
+ "stroke-opacity",
1007
+ String((1 - morphAlpha) * DEFAULT_SKELETON_OPACITY),
1008
+ );
1009
+ }
1010
+ if (morphPathBBuilt) {
1011
+ skeletonPathB.setAttribute("d", morphPathBBuilt);
1012
+ skeletonPathB.setAttribute("visibility", "visible");
1013
+ skeletonPathB.setAttribute("stroke-opacity", String(morphAlpha * DEFAULT_SKELETON_OPACITY));
1014
+ }
1015
+ if (morphAlpha >= 1) {
1016
+ engine.completeMorph();
1017
+ morphResolve?.();
1018
+ morphResolve = null;
1019
+ morphReject = null;
1020
+ morphTarget = null;
1021
+ morphAlpha = 0;
1022
+ morphPathABuilt = "";
1023
+ morphPathBBuilt = "";
1024
+ skeletonPathA.setAttribute("visibility", "hidden");
1025
+ skeletonPathB.setAttribute("visibility", "hidden");
1026
+ const newSkeleton = engine.getSarmalSkeleton();
1027
+ applyBoundaries(newSkeleton);
1028
+ updateSkeleton(newSkeleton);
1029
+ }
1030
+ }
1031
+ const trail = engine.tick(deltaTime);
1032
+ const trailCount = engine.trailCount;
1033
+ if (engine.isLiveSkeleton && engine.morphAlpha === null) {
1034
+ const liveSkeleton = engine.getSarmalSkeleton();
1035
+ applyBoundaries(liveSkeleton);
1036
+ updateSkeleton(liveSkeleton);
1037
+ }
1038
+ updateTrail(trail, trailCount);
1039
+ updateHead(trail, trailCount);
1040
+ }
1041
+ function loop() {
1042
+ const now = performance.now();
1043
+ const deltaTime = Math.min((now - lastTime) / 1e3, 1 / 30);
1044
+ lastTime = now;
1045
+ renderFrame(deltaTime);
1046
+ if (!prefersReducedMotion) {
1047
+ animationId = requestAnimationFrame(loop);
1048
+ }
1049
+ }
1050
+ if (options.initialT !== void 0) {
1051
+ engine.seek(options.initialT);
1052
+ }
1053
+ renderFrame(0);
1054
+ const shouldAutoStart = options.autoStart !== false;
1055
+ const instance = {
1056
+ play() {
1057
+ if (animationId !== null) {
1058
+ return;
1059
+ }
1060
+ lastTime = performance.now();
1061
+ loop();
1062
+ },
1063
+ pause() {
1064
+ if (animationId === null) {
1065
+ return;
1066
+ }
1067
+ cancelAnimationFrame(animationId);
1068
+ animationId = null;
1069
+ engine.cancelSpeedTransition();
1070
+ },
1071
+ reset() {
1072
+ engine.reset();
1073
+ },
1074
+ destroy() {
1075
+ if (animationId !== null) {
1076
+ cancelAnimationFrame(animationId);
1077
+ animationId = null;
1078
+ }
1079
+ if (morphReject !== null) {
1080
+ morphReject(new Error("Instance destroyed during morph"));
1081
+ morphResolve = null;
1082
+ morphReject = null;
1083
+ }
1084
+ group.remove();
1085
+ },
1086
+ ...enginePassthroughs(engine),
1087
+ morphTo(target, options2) {
1088
+ if (morphResolve !== null) {
1089
+ engine.completeMorph();
1090
+ morphResolve();
1091
+ morphResolve = null;
1092
+ morphReject = null;
1093
+ morphAlpha = 0;
1094
+ skeletonPathA.setAttribute("visibility", "hidden");
1095
+ skeletonPathB.setAttribute("visibility", "hidden");
1096
+ }
1097
+ morphDurationMs = options2?.duration ?? DEFAULT_MORPH_DURATION_MS;
1098
+ morphTarget = target;
1099
+ morphAlpha = 0;
1100
+ const currentSkeleton = engine.getSarmalSkeleton();
1101
+ morphPathABuilt = pointsToPathString(currentSkeleton, scale, offsetX, offsetY);
1102
+ engine.startMorph(target, options2?.morphStrategy);
1103
+ if (morphTarget) {
1104
+ const targetSkeleton = sampleCurveSkeleton(target);
1105
+ morphPathBBuilt = pointsToPathString(targetSkeleton, scale, offsetX, offsetY);
1106
+ }
1107
+ return new Promise((resolve, reject) => {
1108
+ morphResolve = resolve;
1109
+ morphReject = reject;
1110
+ });
1111
+ },
1112
+ setRenderOptions(partial) {
1113
+ validateRenderOptions(partial);
1114
+ const prevTrailStyle = trailStyle;
1115
+ if (partial.trailColor !== void 0) {
1116
+ trailColor = partial.trailColor;
1117
+ trailSolid = resolveTrailMainColor(trailColor);
1118
+ trailPalette = resolveTrailPalette(trailColor);
1119
+ if (trailStyle === "default") {
1120
+ for (const p of trailPaths) {
1121
+ p.setAttribute("fill", trailSolid);
1122
+ }
1123
+ }
1124
+ }
1125
+ if (partial.skeletonColor !== void 0) {
1126
+ skeletonColor = partial.skeletonColor;
1127
+ if (skeletonColor === "transparent") {
1128
+ skeletonPath.setAttribute("visibility", "hidden");
1129
+ } else {
1130
+ skeletonPath.setAttribute("stroke", skeletonColor);
1131
+ skeletonPath.removeAttribute("visibility");
1132
+ skeletonPathA.setAttribute("stroke", skeletonColor);
1133
+ skeletonPathB.setAttribute("stroke", skeletonColor);
1134
+ }
1135
+ }
1136
+ if (partial.trailStyle !== void 0) {
1137
+ trailStyle = partial.trailStyle;
1138
+ if (prevTrailStyle !== "default" && trailStyle === "default") {
1139
+ for (const p of trailPaths) {
1140
+ p.setAttribute("fill", trailSolid);
1141
+ }
1142
+ }
1143
+ }
1144
+ if (partial.headColor !== void 0) {
1145
+ userHeadColor = partial.headColor;
1146
+ }
1147
+ if (userHeadColor === null) {
1148
+ headColor = resolveHeadColor(trailColor, trailStyle);
1149
+ } else {
1150
+ headColor = userHeadColor;
1151
+ }
1152
+ headCircle.setAttribute("fill", headColor);
1153
+ if (partial.trailColor !== void 0 || partial.trailStyle !== void 0) {
1154
+ warnIfTrailColorMismatch(trailColor, trailStyle);
1155
+ }
1156
+ },
1157
+ };
1158
+ if (shouldAutoStart) {
1159
+ instance.play();
1160
+ }
1161
+ return instance;
1162
+ }
1163
+ function createSarmalSVG(container, curveDef, options) {
1164
+ const { trailLength, ...rendererOpts } = options ?? {};
1165
+ const engine = createEngine(curveDef, trailLength);
1166
+ return createSVGRenderer({ container, engine, ...rendererOpts });
1167
+ }
1168
+
817
1169
  // src/curves/artemis2.ts
818
1170
  var TWO_PI2 = Math.PI * 2;
819
1171
  function artemis2Fn(t, _time, _params) {
@@ -1099,10 +1451,24 @@ function parseTrailColor(value) {
1099
1451
  } catch {}
1100
1452
  return value;
1101
1453
  }
1454
+ function buildOptions(el2) {
1455
+ return {
1456
+ ...(el2.dataset.trailColor && {
1457
+ trailColor: parseTrailColor(el2.dataset.trailColor),
1458
+ }),
1459
+ ...(el2.dataset.skeletonColor && { skeletonColor: el2.dataset.skeletonColor }),
1460
+ ...(el2.dataset.headColor && { headColor: el2.dataset.headColor }),
1461
+ ...(el2.dataset.headRadius && { headRadius: parseFloat(el2.dataset.headRadius) }),
1462
+ ...(el2.dataset.trailLength && { trailLength: parseInt(el2.dataset.trailLength, 10) }),
1463
+ ...(el2.dataset.trailStyle && {
1464
+ trailStyle: el2.dataset.trailStyle,
1465
+ }),
1466
+ };
1467
+ }
1102
1468
  function init() {
1103
- const canvases = document.querySelectorAll("canvas[data-sarmal]");
1104
- canvases.forEach((canvas) => {
1105
- const curveName = canvas.getAttribute("data-sarmal");
1469
+ const elements = document.querySelectorAll("canvas[data-sarmal], svg[data-sarmal]");
1470
+ elements.forEach((el2) => {
1471
+ const curveName = el2.getAttribute("data-sarmal");
1106
1472
  if (curveName == null) {
1107
1473
  return console.warn("[sarmal] curveName is required");
1108
1474
  }
@@ -1110,20 +1476,13 @@ function init() {
1110
1476
  if (!curveDef) {
1111
1477
  return console.error(`[sarmal] "${curveName}" is not a valid curve name`);
1112
1478
  }
1113
- const instance = createSarmal(canvas, curveDef, {
1114
- ...(canvas.dataset.trailColor && {
1115
- trailColor: parseTrailColor(canvas.dataset.trailColor),
1116
- }),
1117
- ...(canvas.dataset.skeletonColor && { skeletonColor: canvas.dataset.skeletonColor }),
1118
- ...(canvas.dataset.headColor && { headColor: canvas.dataset.headColor }),
1119
- ...(canvas.dataset.headRadius && { headRadius: parseFloat(canvas.dataset.headRadius) }),
1120
- ...(canvas.dataset.trailLength && { trailLength: parseInt(canvas.dataset.trailLength, 10) }),
1121
- ...(canvas.dataset.trailStyle && {
1122
- trailStyle: canvas.dataset.trailStyle,
1123
- }),
1124
- });
1125
- if (canvas.dataset.speed) {
1126
- instance.setSpeed(parseFloat(canvas.dataset.speed));
1479
+ const options = buildOptions(el2);
1480
+ const instance =
1481
+ el2 instanceof HTMLCanvasElement
1482
+ ? createSarmal(el2, curveDef, options)
1483
+ : createSarmalSVG(el2, curveDef, options);
1484
+ if (el2.dataset.speed) {
1485
+ instance.setSpeed(parseFloat(el2.dataset.speed));
1127
1486
  }
1128
1487
  });
1129
1488
  }