
Math Curve Loaders is a loading animation library that provides a collection of 20+ mathematical curve-based SVG loading effects with plain HTML, CSS, and JavaScript.
Features
- Renders particle trails along parametric math curves, including rose curves, Lissajous figures, hypotrochoids, cardioids, Cassini ovals, and Fourier-style paths.
- Animates a configurable number of particles along each curve, with independent control over trail length, loop duration, and pulse timing.
- Rotates the entire loader group on a separate, independently timed cycle.
- Pulses the curve shape over time by modulating the detail scale through a sine function.
How to Use It
1. Clone or download the repository and open index.html directly in a browser.
# Clone the repository git clone https://github.com/[author]/math-curve-loaders.git # Open the demo directly open index.html
2. The index page lists the full loader collection. Click any loader card to open a modal window.
3. Each loader exposes numeric controls in the modal:
Particles(integer): Sets the total number of SVG circle elements rendered along the curve trail. Higher values produce a denser, smoother trail at a small CPU cost.Trail(float): Controls the fraction of the full curve cycle occupied by the particle trail. A value of0.30means each particle spans 30% of the total path behind the lead particle.Loop(seconds): Sets the duration of one full traversal of the curve. This governs the animation’s base speed.Pulse(seconds): Sets the duration of one full pulse cycle. The pulse drives the sine-based modulation that morphs the curve shape over time.Rotate(seconds): Sets the duration of one full rotation of the entire loader group around the SVG center point.Stroke(float): Controls the visual weight of the background path that traces the full curve outline.a(float): Sets the primary amplitude coefficient in the parametric equation. This is the main variable that defines the curve’s fundamental shape.a boost(float): Applies an additive offset toathat expands the curve’s spatial extent.Base pulse(float): Sets the minimum detail scale value reached at the lowest point of the pulse cycle.Pulse boost(float): Sets the amplitude of the pulse modulation added on top of the base value.Scale(float): Multiplies all computed x/y coordinates, scaling the entire curve up or down inside the SVG viewport.
4. Once you’ve dialed in a configuration you like, use the modal’s Download button to download the loader as a standalone HTML file. The Copy button places the full source code on the clipboard.
5. Paste the exported code into your project.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rose Three</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #050505;
color: #f5f5f5;
font-family: Inter, system-ui, sans-serif;
}
.demo {
display: grid;
gap: 20px;
justify-items: center;
padding: 32px;
}
.frame {
width: min(72vmin, 420px);
aspect-ratio: 1;
display: grid;
place-items: center;
}
svg {
width: 100%;
height: 100%;
overflow: visible;
}
.meta {
display: grid;
gap: 6px;
text-align: center;
}
.title {
font-size: 22px;
font-weight: 700;
}
.tag {
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(255,255,255,0.58);
}
.formula {
max-width: min(92vw, 720px);
padding: 14px 16px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 14px;
background: rgba(255,255,255,0.03);
color: rgba(255,255,255,0.82);
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
white-space: pre-wrap;
}
.back-link {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.14);
background: rgba(255,255,255,0.04);
color: #fff;
text-decoration: none;
font-size: 13px;
line-height: 1;
transition: background 180ms ease, border-color 180ms ease, transform 180ms ease;
}
.back-link:hover {
background: rgba(255,255,255,0.08);
border-color: rgba(255,255,255,0.22);
transform: translateY(-1px);
}
</style>
</head>
<body>
<div class="demo">
<div class="frame">
<svg viewBox="0 0 100 100" fill="none" aria-hidden="true">
<g id="group">
<path id="path" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" opacity="0.1"></path>
</g>
</svg>
</div>
<div class="meta">
<div class="title">Rose Three</div>
<div class="tag">r = a cos(3θ)</div>
</div>
<pre class="formula" id="formula"></pre>
</div>
<script>
const SVG_NS = 'http://www.w3.org/2000/svg';
const config = {
name: "Rose Three",
tag: "r = a cos(3θ)",
rotate: true,
particleCount: 76,
trailSpan: 0.31,
durationMs: 5300,
rotationDurationMs: 28000,
pulseDurationMs: 4400,
strokeWidth: 4.6,
roseA: 9.2,
roseABoost: 0.6,
roseBreathBase: 0.72,
roseBreathBoost: 0.28,
roseScale: 3.25,
formula(config) {
return [
`r(t) = (${config.roseA.toFixed(1)} + ${config.roseABoost.toFixed(2)}s)(${config.roseBreathBase.toFixed(2)} + ${config.roseBreathBoost.toFixed(2)}s) cos(3t)`,
`x(t) = 50 + cos t · r(t) · ${config.roseScale.toFixed(2)}`,
`y(t) = 50 + sin t · r(t) · ${config.roseScale.toFixed(2)}`,
].join("\n");
},
point(progress, detailScale, config) {
const t = progress * Math.PI * 2;
const a = config.roseA + detailScale * config.roseABoost;
const r = a * (config.roseBreathBase + detailScale * config.roseBreathBoost) * Math.cos(3 * t);
return {
x: 50 + Math.cos(t) * r * config.roseScale,
y: 50 + Math.sin(t) * r * config.roseScale,
};
},
};
const group = document.querySelector('#group');
const path = document.querySelector('#path');
const formula = document.querySelector('#formula');
path.setAttribute('stroke-width', String(config.strokeWidth));
formula.textContent = typeof config.formula === 'function' ? config.formula(config) : config.formula;
const particles = Array.from({ length: config.particleCount }, () => {
const circle = document.createElementNS(SVG_NS, 'circle');
circle.setAttribute('fill', 'currentColor');
group.appendChild(circle);
return circle;
});
function normalizeProgress(progress) {
return ((progress % 1) + 1) % 1;
}
function getDetailScale(time) {
const pulseProgress = (time % config.pulseDurationMs) / config.pulseDurationMs;
const pulseAngle = pulseProgress * Math.PI * 2;
return 0.52 + ((Math.sin(pulseAngle + 0.55) + 1) / 2) * 0.48;
}
function getRotation(time) {
if (!config.rotate) return 0;
return -((time % config.rotationDurationMs) / config.rotationDurationMs) * 360;
}
function buildPath(detailScale, steps = 480) {
return Array.from({ length: steps + 1 }, (_, index) => {
const point = config.point(index / steps, detailScale, config);
return `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
}).join(' ');
}
function getParticle(index, progress, detailScale) {
const tailOffset = index / (config.particleCount - 1);
const point = config.point(normalizeProgress(progress - tailOffset * config.trailSpan), detailScale, config);
const fade = Math.pow(1 - tailOffset, 0.56);
return {
x: point.x,
y: point.y,
radius: 0.9 + fade * 2.7,
opacity: 0.04 + fade * 0.96,
};
}
const startedAt = performance.now();
function render(now) {
const time = now - startedAt;
const progress = (time % config.durationMs) / config.durationMs;
const detailScale = getDetailScale(time);
group.setAttribute('transform', `rotate(${getRotation(time)} 50 50)`);
path.setAttribute('d', buildPath(detailScale));
particles.forEach((node, index) => {
const particle = getParticle(index, progress, detailScale);
node.setAttribute('cx', particle.x.toFixed(2));
node.setAttribute('cy', particle.y.toFixed(2));
node.setAttribute('r', particle.radius.toFixed(2));
node.setAttribute('opacity', particle.opacity.toFixed(3));
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>Alternatives:
FAQs:
Q: Can I change the loader color?
A: Set the color attribute on the #rotating-group element and the stroke attribute on #background-path.
Q: Why does my exported loader look different from the modal preview?
A: The modal preview reads the live slider values at export time. If you adjusted a slider and then immediately clicked export, verify that the numeric fields updated before the export fired.
Q: Can I run multiple loaders on the same page?
A: Yes, but each instance runs its own requestAnimationFrame loop. For pages with more than two or three active loaders, consider pausing off-screen instances with an IntersectionObserver to keep CPU usage reasonable.






