library / backgrounds / rotating-spiral
live - 60fps 1920 x 1080
rotating-spiral.html - self-contained
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Rotating Spiral</title>
  <style>
    /* Fullscreen dark backdrop */
    html, body {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #111;
    }
    canvas {
      display: block;
      margin: auto;
    }
    span {
      color: #777;
      position: absolute;
      top: 20px;
      left: 20px;
      user-select: none;
      font-family: sans-serif;
    }
  </style>
</head>
<body>
<canvas></canvas>
<span>drag to rotate</span>
<script>
/*
 * Rotating spiral animation
 *
 * This script recreates the original CodePen example (QdWpRv) by Hakim El Hattab.
 * A series of points form a spiral that rotates in response to mouse or touch
 * interaction. The animation is drawn on a full‑screen canvas and uses simple
 * trigonometry to position the points along the spiral. Dragging the mouse
 * horizontally or vertically influences the rotation speed and direction.
 */
(function () {
  const canvas = document.querySelector('canvas');
  const context = canvas.getContext('2d');

  let time = 0;
  let velocity = 0.1;
  let velocityTarget = 0.1;
  let width;
  let height;
  let lastX;
  let lastY;

  const MAX_OFFSET = 400;
  const SPACING = 4;
  const POINTS = MAX_OFFSET / SPACING;
  const PEAK = MAX_OFFSET * 0.25;
  const POINTS_PER_LAP = 6;
  const SHADOW_STRENGTH = 6;

  function resize() {
    width = canvas.width = window.innerWidth;
    height = canvas.height = window.innerHeight;
  }

  function step() {
    time += velocity;
    velocity += (velocityTarget - velocity) * 0.3;

    clear();
    render();

    requestAnimationFrame(step);
  }

  function clear() {
    context.clearRect(0, 0, width, height);
  }

  function render() {
    const cx = width / 2;
    const cy = height / 2;
    context.globalCompositeOperation = 'lighter';
    context.strokeStyle = '#fff';
    context.shadowColor = '#fff';
    context.lineWidth = 2;
    context.beginPath();
    for (let i = POINTS; i > 0; i--) {
      const value = i * SPACING + (time % SPACING);
      const ax = Math.sin(value / POINTS_PER_LAP) * Math.PI;
      const ay = Math.cos(value / POINTS_PER_LAP) * Math.PI;
      let x = ax * value;
      let y = ay * value * 0.35;
      const o = 1 - (Math.min(value, PEAK) / PEAK);
      y -= Math.pow(o, 2) * 200;
      y += 200 * value / MAX_OFFSET;
      y += (x / cx) * width * 0.1;
      context.globalAlpha = 1 - (value / MAX_OFFSET);
      context.shadowBlur = SHADOW_STRENGTH * o;
      context.lineTo(cx + x, cy + y);
      context.stroke();
      context.beginPath();
      context.moveTo(cx + x, cy + y);
    }
    context.lineTo(cx, cy - 200);
    context.lineTo(cx, 0);
    context.stroke();
  }

  function onMouseDown(event) {
    lastX = event.clientX;
    lastY = event.clientY;
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  }

  function onMouseMove(event) {
    let vx = (event.clientX - lastX) / 100;
    let vy = (event.clientY - lastY) / 100;
    if (event.clientY < height / 2) vx *= -1;
    if (event.clientX > width / 2) vy *= -1;
    velocityTarget = vx + vy;
    lastX = event.clientX;
    lastY = event.clientY;
  }

  function onMouseUp() {
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  }

  function onTouchStart(event) {
    event.preventDefault();
    lastX = event.touches[0].clientX;
    lastY = event.touches[0].clientY;
    document.addEventListener('touchmove', onTouchMove);
    document.addEventListener('touchend', onTouchEnd);
  }

  function onTouchMove(event) {
    let vx = (event.touches[0].clientX - lastX) / 100;
    let vy = (event.touches[0].clientY - lastY) / 100;
    if (event.touches[0].clientY < height / 2) vx *= -1;
    if (event.touches[0].clientX > width / 2) vy *= -1;
    velocityTarget = vx + vy;
    lastX = event.touches[0].clientX;
    lastY = event.touches[0].clientY;
  }

  function onTouchEnd() {
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
  }

  // Initialize the canvas and event listeners
  resize();
  step();
  window.addEventListener('resize', resize);
  window.addEventListener('mousedown', onMouseDown);
  document.addEventListener('touchstart', onTouchStart);
})();
</script>
</body>
</html>

About this animation

Classic rotating spiral illusion.

Under the hood

Golden Ratio Drawing

Utilizes the mathematical Fibonacci sequence or Golden Ratio angles. The points are drawn outward exponentially, rotating on the core axis. The human eye finds these procedural shapes incredibly satisfying due to their natural geometric aesthetics.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger