Mathematical SVG Loading Animations in CSS & JS – Math Curve Loaders

Category: Javascript , Loading , Recommended | April 7, 2026
AuthorPaidax01
Last UpdateApril 7, 2026
LicenseMIT
Views21 views
Mathematical SVG Loading Animations in CSS & JS – Math Curve Loaders

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 of 0.30 means 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 to a that 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.

You Might Be Interested In:


Leave a Reply