← Mike Higgins Home
Custom Code Experiment •

Vortex: SVG

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vortex: SVG</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      background: radial-gradient(circle at 50% 50%, #000814 0%, #000000 100%);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
        Ubuntu, Cantarell, sans-serif;
      overflow: hidden;
      cursor: crosshair;
    }

    .container {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100vh;
      position: relative;
    }

    svg {
      width: 100vw;
      height: 100vh;
      filter: drop-shadow(0 0 40px rgba(0, 200, 255, 0.4));
    }

    .spiral-path {
      fill: none;
      stroke-linecap: round;
      animation: dash 3s linear infinite;
    }

    @keyframes dash {
      to {
        stroke-dashoffset: -1000;
      }
    }

    .particle {
      animation: orbit 4s linear infinite;
    }

    @keyframes orbit {
      to {
        transform: rotate(360deg);
      }
    }

    .core-ring {
      fill: none;
      stroke-linecap: round;
      animation: pulse 2s ease-in-out infinite;
    }

    @keyframes pulse {
      0%,
      100% {
        opacity: 0.6;
        transform: scale(1);
      }
      50% {
        opacity: 1;
        transform: scale(1.1);
      }
    }

    .trail {
      position: absolute;
      width: 4px;
      height: 4px;
      background: radial-gradient(
        circle,
        rgba(255, 255, 255, 0.9) 0%,
        transparent 70%
      );
      border-radius: 50%;
      pointer-events: none;
      animation: fade-out 1s ease-out forwards;
    }

    @keyframes fade-out {
      to {
        opacity: 0;
        transform: scale(0);
      }
    }

    @media (max-width: 768px) {
      svg {
        filter: drop-shadow(0 0 30px rgba(0, 200, 255, 0.3));
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .spiral-path,
      .particle,
      .core-ring {
        animation: none;
      }
    }
  </style>
</head>

<body>
  <div class="container">
    <svg id="vortex" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <filter id="glow">
          <feGaussianBlur stdDeviation="4" result="coloredBlur" />
          <feMerge>
            <feMergeNode in="coloredBlur" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>

        <radialGradient id="vortexGrad">
          <stop offset="0%" class="grad-inner" style="stop-color: rgb(0, 200, 255)" />
          <stop offset="50%" class="grad-mid" style="stop-color: rgb(100, 100, 255)" />
          <stop offset="100%" class="grad-outer" style="stop-color: rgb(200, 0, 255)" />
        </radialGradient>
      </defs>

      <g id="core-rings">
        <circle class="core-ring" cx="50%" cy="50%" r="30" stroke="url(#vortexGrad)" stroke-width="2" opacity="0.8" />
        <circle class="core-ring" cx="50%" cy="50%" r="50" stroke="url(#vortexGrad)" stroke-width="1.5" opacity="0.6" style="animation-delay: -0.5s" />
        <circle class="core-ring" cx="50%" cy="50%" r="70" stroke="url(#vortexGrad)" stroke-width="1" opacity="0.4" style="animation-delay: -1s" />
      </g>

      <g id="spirals" filter="url(#glow)"></g>
      <g id="particles" filter="url(#glow)"></g>
    </svg>
  </div>

  <script>
    const svg = document.getElementById("vortex");
    const spiralsGroup = document.getElementById("spirals");
    const particlesGroup = document.getElementById("particles");

    let width = window.innerWidth;
    let height = window.innerHeight;

    svg.setAttribute("viewBox", `0 0 ${width} ${height}`);

    window.addEventListener("resize", () => {
      width = window.innerWidth;
      height = window.innerHeight;
      svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
    });

    let time = 0;
    let themeIndex = 0;

    const themes = [
      { inner: [0, 200, 255], mid: [100, 100, 255], outer: [200, 0, 255] },
      { inner: [255, 50, 0], mid: [255, 150, 0], outer: [255, 200, 0] },
      { inner: [0, 255, 150], mid: [0, 200, 200], outer: [0, 100, 255] },
      { inner: [255, 0, 200], mid: [200, 0, 255], outer: [150, 0, 255] },
      { inner: [255, 200, 0], mid: [255, 255, 100], outer: [200, 255, 0] }
    ];

    const numSpirals = 6;
    for (let i = 0; i < numSpirals; i++) {
      const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
      path.classList.add("spiral-path");
      path.setAttribute("stroke", "url(#vortexGrad)");
      path.setAttribute("stroke-width", 2.5 - i * 0.3);
      path.setAttribute("opacity", 0.7 - i * 0.1);
      path.setAttribute("stroke-dasharray", "800");
      path.style.animationDelay = `-${i * 0.5}s`;

      const points = [];
      const rotationOffset = (i / numSpirals) * Math.PI * 2;

      for (let j = 0; j < 50; j++) {
        const progress = j / 50;
        const angle = progress * Math.PI * 6 + rotationOffset;
        const radius = progress * 500;

        const x = width / 2 + Math.cos(angle) * radius;
        const y = height / 2 + Math.sin(angle) * radius;

        points.push(`${j === 0 ? "M" : "L"} ${x} ${y}`);
      }

      path.setAttribute("d", points.join(" "));
      spiralsGroup.appendChild(path);
    }

    for (let i = 0; i < 4; i++) {
      const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
      g.classList.add("particle");
      g.style.transformOrigin = `${width / 2}px ${height / 2}px`;
      g.style.animationDelay = `-${i}s`;

      for (let j = 0; j < 10; j++) {
        const progress = j / 10;
        const angle = progress * Math.PI * 2;
        const radius = 150 + progress * 350;

        const x = width / 2 + Math.cos(angle) * radius;
        const y = height / 2 + Math.sin(angle) * radius;

        const circle = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "circle"
        );
        circle.setAttribute("cx", x);
        circle.setAttribute("cy", y);
        circle.setAttribute("r", 2.5 * (1 - progress) + 1);
        circle.setAttribute("fill", "url(#vortexGrad)");
        circle.setAttribute("opacity", (1 - progress) * 0.7);

        g.appendChild(circle);
      }

      particlesGroup.appendChild(g);
    }

    setInterval(() => {
      const spirals = document.querySelectorAll(".spiral-path");
      const rings = document.querySelectorAll(".core-ring");

      spirals.forEach((spiral) => {
        spiral.style.transition =
          "stroke-width 0.6s ease-out, opacity 0.6s ease-out";
        const currentWidth = parseFloat(spiral.getAttribute("stroke-width"));
        spiral.setAttribute("stroke-width", currentWidth * 1.5);
        spiral.setAttribute(
          "opacity",
          parseFloat(spiral.getAttribute("opacity")) * 1.3
        );
      });

      rings.forEach((ring) => {
        ring.style.transition = "r 0.6s ease-out";
        const currentR = parseFloat(ring.getAttribute("r"));
        ring.setAttribute("r", currentR * 1.4);
      });

      setTimeout(() => {
        spirals.forEach((spiral, i) => {
          spiral.setAttribute("stroke-width", 2.5 - i * 0.3);
          spiral.setAttribute("opacity", 0.7 - i * 0.1);
        });

        rings.forEach((ring, i) => {
          ring.setAttribute("r", 30 + i * 20);
        });
      }, 1200);
    }, 5000);

    setInterval(() => {
      themeIndex = (themeIndex + 1) % themes.length;
      const theme = themes[themeIndex];

      document.querySelector(
        ".grad-inner"
      ).style.stopColor = `rgb(${theme.inner[0]}, ${theme.inner[1]}, ${theme.inner[2]})`;
      document.querySelector(
        ".grad-mid"
      ).style.stopColor = `rgb(${theme.mid[0]}, ${theme.mid[1]}, ${theme.mid[2]})`;
      document.querySelector(
        ".grad-outer"
      ).style.stopColor = `rgb(${theme.outer[0]}, ${theme.outer[1]}, ${theme.outer[2]})`;

      document.querySelector(".grad-inner").style.transition = "stop-color 2s ease";
      document.querySelector(".grad-mid").style.transition = "stop-color 2s ease";
      document.querySelector(".grad-outer").style.transition = "stop-color 2s ease";
    }, 10000);

    document.addEventListener("click", (e) => {
      for (let i = 0; i < 20; i++) {
        const particle = document.createElement("div");
        particle.className = "trail";
        particle.style.left = e.clientX + "px";
        particle.style.top = e.clientY + "px";

        const angle = (Math.PI * 2 * i) / 20;
        const velocity = 80 + Math.random() * 150;
        const vx = Math.cos(angle) * velocity;
        const vy = Math.sin(angle) * velocity;

        const hue = (themeIndex * 72 + Math.random() * 60) % 360;
        particle.style.background = `radial-gradient(circle, hsla(${hue}, 100%, 70%, 0.9) 0%, transparent 70%)`;

        document.body.appendChild(particle);

        particle.animate(
          [
            { transform: "translate(0, 0) scale(1)", opacity: 1 },
            { transform: `translate(${vx}px, ${vy}px) scale(0)`, opacity: 0 }
          ],
          {
            duration: 1000,
            easing: "ease-out"
          }
        );

        setTimeout(() => particle.remove(), 1000);
      }
    });

    document.addEventListener("keydown", (e) => {
      if (e.code === "Space") {
        e.preventDefault();
        const animations = document.querySelectorAll(
          ".spiral-path, .particle, .core-ring"
        );
        animations.forEach((el) => {
          if (el.style.animationPlayState === "paused") {
            el.style.animationPlayState = "running";
          } else {
            el.style.animationPlayState = "paused";
          }
        });
      }
    });
  </script>
</body>

</html>