Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/aurora-borealis/llms.txt

Use this file to discover all available pages before exploring further.

The Aurora Borealis portfolio runs two completely independent animation systems in parallel: a background layer composed of Canvas 2D rendering and Framer Motion gradient blobs that produce the ambient night-sky atmosphere, and a foreground layer of Framer Motion page transitions that respond to navigation. Neither system blocks the other — Canvas updates happen inside their own requestAnimationFrame loop while Framer Motion drives its motion.div elements through its own internal scheduler.

Animation Layer Overview

LayerTechnologyComponentsTrigger
Background atmosphereFramer Motion + CSSAuroraBackgroundContinuous loop on mount
Star fieldCanvas 2D + requestAnimationFrameStarFieldContinuous loop on mount
Page enter / exitFramer MotionPageTransitionReact Router pathname change
All background animation elements carry pointer-events-none on their root node, ensuring that the continuously animating layers never absorb mouse or touch events meant for interactive page content.

AuroraBackground

AuroraBackground is a fixed, full-viewport div positioned at z-0 with opacity-60 applied to the entire component. It renders three motion.div gradient layers and one SVG wave, all of which loop indefinitely.

The Three Gradient Layers

Each layer is an absolutely-positioned motion.div carrying a radial-gradient background, a backdrop-filter or filter blur, and a looping animate prop that shifts its x, y, and scale values.
// Gradient layer configuration inside AuroraBackground

// Layer 1 — Teal, 20 s loop
<motion.div
  style={{
    background:
      "radial-gradient(ellipse at center, rgba(45, 212, 191, 0.15) 0%, rgba(20, 184, 166, 0.05) 40%, transparent 70%)",
    filter: "blur(60px)",
  }}
  animate={{
    x: ["-20%", "20%", "-20%"],
    y: ["-10%", "10%", "-10%"],
    scale: [1, 1.2, 1],
  }}
  transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
/>

// Layer 2 — Cyan + Violet, 25 s loop, screen blend
<motion.div
  style={{
    background:
      "radial-gradient(ellipse at center, rgba(34, 211, 238, 0.15) 0%, rgba(139, 92, 246, 0.1) 40%, transparent 70%)",
    filter: "blur(80px)",
    mixBlendMode: "screen",
  }}
  animate={{
    x: ["20%", "-20%", "20%"],
    y: ["10%", "-10%", "10%"],
    scale: [1.2, 1, 1.2],
  }}
  transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
/>

// Layer 3 — Magenta, 30 s loop, screen blend
<motion.div
  style={{
    background:
      "radial-gradient(ellipse at center, rgba(236, 72, 153, 0.1) 0%, transparent 50%)",
    filter: "blur(100px)",
    mixBlendMode: "screen",
  }}
  animate={{
    x: ["0%", "30%", "0%", "-30%", "0%"],
    y: ["20%", "0%", "-20%", "0%", "20%"],
  }}
  transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
/>

Layer 1 — Teal

Colors rgba(45, 212, 191, 0.15)rgba(20, 184, 166, 0.05). Blur: 60 px. Loop duration: 20 s. No blend mode override — renders as standard alpha compositing.

Layer 2 — Cyan / Violet

Colors rgba(34, 211, 238, 0.15)rgba(139, 92, 246, 0.1). Blur: 80 px. Loop duration: 25 s. mix-blend-mode: screen brightens intersections with layer 1.

Layer 3 — Magenta

Color rgba(236, 72, 153, 0.1) → transparent. Blur: 100 px. Loop duration: 30 s. mix-blend-mode: screen adds a warm pink tint where it overlaps the cooler layers.

Blend mode result

The screen blend on layers 2 and 3 means overlapping regions become brighter rather than muddier, producing the luminous quality characteristic of real aurora curtains against a dark sky.

SVG Wave Animation

Below the gradient layers, AuroraBackground renders an SVG element containing a <path> whose d attribute is animated between three keyframe shapes using Framer Motion. The path is filled using a <linearGradient> with id aurora-grad:
// SVG wave with animated path and linearGradient fill
<svg className="absolute inset-0 w-full h-full opacity-30" preserveAspectRatio="none">
  <defs>
    <linearGradient id="aurora-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%"   stopColor="#2DD4BF" stopOpacity="0.4" />
      <stop offset="50%"  stopColor="#22D3EE" stopOpacity="0.2" />
      <stop offset="100%" stopColor="#8B5CF6" stopOpacity="0.4" />
    </linearGradient>
  </defs>
  <motion.path
    fill="url(#aurora-grad)"
    d="M0,50 Q25,30 50,50 T100,50 L100,100 L0,100 Z"
    animate={{
      d: [
        "M0,60 Q25,40 50,60 T100,60 L100,100 L0,100 Z",
        "M0,40 Q25,60 50,40 T100,40 L100,100 L0,100 Z",
        "M0,60 Q25,40 50,60 T100,60 L100,100 L0,100 Z",
      ],
    }}
    transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
  />
</svg>
The gradient sweeps diagonally from teal (#2DD4BF) through cyan (#22D3EE) to violet (#8B5CF6), matching the color palette of the motion.div layers above it. The wave oscillates on a 15-second loop, creating a slow undulating horizon at the base of the aurora.
Animating the SVG d attribute between keyframe strings works in Framer Motion as long as all keyframe paths have the same number of SVG commands and points. Changing the command count mid-animation will cause a hard snap rather than a smooth morph.

StarField

StarField is a Canvas 2D renderer. It never uses Framer Motion; all animation is driven by a manual requestAnimationFrame loop.

Initialisation

When the component mounts (or the window resizes), it:
  1. Sets the canvas width and height to window.innerWidth and window.innerHeight.
  2. Calculates star count as Math.floor(width * height / 4000).
  3. Generates each star with random x, y, a size between 0.5 and 2 px, and a twinkle speed between 0.005 and 0.025.

Per-Frame Update

On each animation frame the renderer:
  • Clears the canvas with clearRect.
  • Advances each star’s twinkle phase, producing an opacity that oscillates continuously.
  • Drifts each star upward by a small increment (slow upward movement simulating perspective drift through the atmosphere).
  • Tests each frame for a shooting-star spawn: there is a 0.5 % chance per frame that a new shooting star is created, subject to a hard cap of three active shooting stars at any time.
  • Draws each shooting star as a gradient-filled line that fades out as its opacity value decreases.
  • Draws each regular star as a filled arc in rgba(236, 254, 255, opacity) — a near-white cyan that reads as starlight on the dark background.
// Shooting star spawn logic (canvas renderer excerpt)
// Spawn condition: Math.random() > 0.995 (≈ 0.5% chance per frame)
const MAX_SHOOTING = 3;

function maybeSpawnShootingStar(shootingStars, width) {
  if (shootingStars.length < MAX_SHOOTING && Math.random() > 0.995) {
    shootingStars.push({
      x: Math.random() * width,
      y: 0,                                          // spawns at top edge
      length: Math.random() * 80 + 20,
      speed: Math.random() * 10 + 5,
      angle: Math.PI / 4 + (Math.random() * 0.2 - 0.1), // ~45° diagonal
      opacity: 1.0,
      active: true,
    });
  }
}

function drawShootingStar(ctx, star) {
  const tailX = star.x - Math.cos(star.angle) * star.length;
  const tailY = star.y - Math.sin(star.angle) * star.length;
  const grad = ctx.createLinearGradient(star.x, star.y, tailX, tailY);
  grad.addColorStop(0, `rgba(236, 254, 255, ${star.opacity})`);
  grad.addColorStop(1, "rgba(236, 254, 255, 0)");

  ctx.beginPath();
  ctx.moveTo(star.x, star.y);
  ctx.lineTo(tailX, tailY);
  ctx.strokeStyle = grad;
  ctx.lineWidth = 2;
  ctx.stroke();

  // Advance position and decay opacity
  star.x += Math.cos(star.angle) * star.speed;
  star.y += Math.sin(star.angle) * star.speed;
  star.opacity -= 0.015;

  // Mark inactive when faded or off-screen
  if (star.opacity <= 0 || star.x > ctx.canvas.width || star.y > ctx.canvas.height) {
    star.active = false;
  }
}

Resize Handling

StarField attaches a resize event listener on window. When a resize fires it tears down the current star array and shooting-star list, recalculates the canvas dimensions and new star count, and rebuilds from scratch before resuming the requestAnimationFrame loop.

PageTransition

PageTransition is the foreground animation component. It wraps every <Route> element and is keyed by location.pathname inside <AnimatePresence mode="wait">, so React fully unmounts the old instance before mounting the new one.
// PageTransition component definition
import { motion } from "framer-motion";

const pageVariants = {
  initial: { opacity: 0, filter: "blur(10px)" },
  animate: { opacity: 1, filter: "blur(0px)"  },
  exit:    { opacity: 0, filter: "blur(10px)" },
};

const pageTransition = {
  duration: 0.8,
  ease: "easeInOut",
};

export function PageTransition({ children }) {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={pageTransition}
    >
      {children}
    </motion.div>
  );
}
The animation blurs the page in and out (blur(10px)blur(0px)) while simultaneously fading opacity over 800 ms. The blur mirrors the soft-focus look of the aurora backdrop, making navigation feel like the scene comes into and out of focus rather than cutting hard between destinations.
AnimatePresence mode="wait" guarantees the exit animation completes before the enter animation starts. Removing mode="wait" or omitting the key prop on <Routes> would cause the old and new pages to animate simultaneously, which breaks the intended visual effect.

Performance Considerations

pointer-events-none

Both AuroraBackground and StarField set pointer-events-none on their root elements. This prevents the continuously-painting canvas and animating motion.div layers from consuming any input events, keeping the foreground fully interactive.

Framer Motion will-change

Framer Motion automatically applies will-change: transform to elements it is actively animating. For the three motion.div aurora layers this hints to the browser compositor that those elements should be promoted to their own GPU layers, avoiding layout and paint invalidation on every frame.

Separate RAF loops

The StarField canvas loop and Framer Motion’s internal scheduler run independently. A slow frame in the canvas renderer does not delay Framer Motion tweens, and vice versa. This isolation prevents canvas jank from impacting page transition smoothness.

No canvas compositing overhead

The canvas element is placed behind all motion.div layers in the stacking context. The browser composites the canvas as a single opaque layer underneath, meaning the aurora gradient blends happen in CSS compositing rather than forcing a canvas readback.
On low-end devices, running three simultaneously animating motion.div layers plus a full-screen canvas requestAnimationFrame loop can strain the GPU. Consider reducing opacity on AuroraBackground or lowering the star-density divisor (currently 4000) for a lighter visual load on constrained hardware.

Build docs developers (and LLMs) love