Skip to main content

Documentation Index

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

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

Framer Motion is the animation engine powering nearly every interactive moment in Digital Domain — from the CRT-flicker that fires on every route change, to the stat bars that fill like an RPG level-up, to the Winamp visualizer bars that oscillate endlessly in the background. Animations are used sparingly but deliberately: each one reinforces the retro computing aesthetic while keeping the UI feeling alive and responsive.
All Framer Motion components (motion.div, motion.button, motion.form, AnimatePresence) are imported from assets/proxy.js, a shared vendor chunk. This avoids duplicating the Framer Motion runtime across multiple page component chunks and keeps the total bundle size lean.

Page Transitions

Every page change in Digital Domain is wrapped in a Framer Motion AnimatePresence + motion.div pair inside the Layout shell. The motion.div is keyed by location.pathname, which tells Framer Motion to treat each navigation as a new element — triggering the exit animation on the outgoing page before the incoming page enters. The transition is designed to look like a CRT monitor briefly over-brightening and then settling, matching the @keyframes crt flash described below.
import { AnimatePresence, motion } from "../assets/proxy.js";
import { Outlet, useLocation } from "react-router-dom";

function Layout() {
  const location = useLocation();
  const [isFlickering, setIsFlickering] = useState(false);

  useEffect(() => {
    setIsFlickering(true);
    const timer = setTimeout(() => setIsFlickering(false), 150);
    return () => clearTimeout(timer);
  }, [location.pathname]);

  return (
    <div className="min-h-screen flex flex-col">
      <div className="scanlines" />
      <header>{/* nav */}</header>

      <main className="flex-1 relative p-4 md:p-8">
        <AnimatePresence mode="wait">
          <motion.div
            key={location.pathname}
            initial={{ opacity: 0, scale: 0.98, filter: "brightness(2) contrast(1.5)" }}
            animate={{ opacity: 1, scale: 1,    filter: "brightness(1) contrast(1)"   }}
            exit={{    opacity: 0, scale: 1.02,  filter: "brightness(0.5) contrast(2)" }}
            transition={{ duration: 0.2, ease: "easeInOut" }}
            className={`h-full ${isFlickering ? "animate-crt-flicker" : ""}`}
          >
            <Outlet />
          </motion.div>
        </AnimatePresence>
      </main>
    </div>
  );
}

Animation Variant Values

Propertyinitialanimateexit
opacity010
scale0.9811.02
filter (brightness)210.5
filter (contrast)1.512
transition.duration0.2s
transition.easeeaseInOut
The slight scale-down on enter (0.98) and scale-up on exit (1.02) combined with the brightness/contrast filter changes creates a convincing “CRT power cycle” feeling — the screen briefly flares white on entry and dims to black on exit.
The mode="wait" prop on AnimatePresence is critical: it ensures the exiting page’s animation completes fully before the entering page begins its initialanimate transition. Without mode="wait", both pages would animate simultaneously, causing a jarring overlap.

CRT Flicker Effect

Independently from the Framer Motion variants, a CSS @keyframes animation called crt runs for 150ms on every route change. It is triggered by adding the animate-crt-flicker Tailwind class to the motion.div wrapper via a useState flag that is set on location.pathname change and cleared with setTimeout.
@keyframes crt {
  0%   { opacity: 0;   transform: scale(0.95) skew(2deg);  }
  20%  { opacity: 1;   transform: scale(1.02) skew(-2deg); }
  40%  { opacity: 0.8; transform: scale(0.98) skew(1deg);  }
  60%  { opacity: 1;   transform: scale(1.01) skew(-1deg); }
  80%  { opacity: 0.9; transform: scale(0.99) skew(0);     }
  100% { opacity: 1;   transform: scale(1)    skew(0);     }
}

.animate-crt-flicker {
  animation: crt 0.15s ease-in-out;
}
The keyframe sequence oscillates opacity between 0 and 1 and applies small scale and skew jitters, faithfully recreating the brief geometric distortion visible on a CRT when the input signal is interrupted.

Animated Stat Bars

The /about page displays three RPG-style stat bars (HP, MP, XP) that animate from zero width to their target percentage when the component mounts. Each bar is a motion.div nested inside a fixed-height track.
function StatBar({ label, color, percentage }) {
  return (
    <div>
      <div className="text-white font-vt323 text-sm mb-1">{label}</div>
      <div className="h-4 bg-gray-800 border border-gray-600 w-full">
        <motion.div
          initial={{ width: 0 }}
          animate={{ width: `${percentage}%` }}
          transition={{ duration: 1.5, ease: "easeOut" }}
          className={`h-full ${color}`}
        />
      </div>
    </div>
  );
}

{/* Usage */}
<StatBar label="HP (Health)"       color="bg-red-500"       percentage={85} />
<StatBar label="MP (Mana/Coffee)"  color="bg-blue-500"      percentage={40} />
<StatBar label="XP (Experience)"   color="bg-retro-yellow"  percentage={92} />
The 1.5-second easeOut duration feels satisfying without being slow — the bar accelerates quickly and then decelerates as it approaches its final position, mimicking a loading indicator filling up.

Winamp Visualizer Bars

The Winamp widget on the /about page renders 16 animated bars using [...Array(16)].map(...). Each bar is a motion.div with a height keyframe sequence that loops forever with a "mirror" repeat type, creating a smooth back-and-forth oscillation.
<div className="h-12 bg-black border border-gray-700 flex items-end gap-[2px] p-1 overflow-hidden">
  {[...Array(16)].map((_, i) => (
    <motion.div
      key={i}
      className="w-full bg-gradient-to-t from-green-500 via-yellow-500 to-red-500"
      animate={{
        height: ["10%", "90%", "30%", "100%", "20%"],
      }}
      transition={{
        duration: 0.5 + Math.random(), // Each bar has a unique speed
        repeat: Infinity,
        repeatType: "mirror",
      }}
    />
  ))}
</div>
Each bar receives a randomized duration between 0.5s and 1.5s so that no two bars are in sync, producing the organic, chaotic waveform characteristic of a real audio visualizer.
Because Math.random() is called inline in JSX during render, the durations are stable within a single mount but will re-randomize on a full unmount/remount (e.g. navigating away and back). This is intentional — it keeps the visualizer feeling fresh.

Taskbar Item Enter / Exit

The Win98 taskbar on the /skills page uses AnimatePresence to animate window buttons in and out as users open and close skill windows by double-clicking desktop icons. Each button entry fades in while its width expands from zero, and reverses on removal.
import { AnimatePresence, motion } from "framer-motion";

<div className="flex-1 flex gap-1 overflow-x-auto">
  <AnimatePresence>
    {openWindows.map((windowId) => {
      const item = skillItems.find((s) => s.id === windowId);
      const isActive = focusedWindow === windowId;

      return (
        <motion.button
          key={windowId}
          initial={{ opacity: 0, width: 0 }}
          animate={{ opacity: 1, width: "auto" }}
          exit={{    opacity: 0, width: 0 }}
          onClick={() => setFocusedWindow(windowId)}
          className={`
            px-2 py-1 text-sm font-comic flex items-center gap-2
            min-w-[120px] max-w-[150px] truncate h-8
            ${isActive
              ? "shadow-win-btn-active bg-gray-300"
              : "shadow-win-btn bg-win-gray"
            }
          `}
        >
          <item.icon size={14} />
          <span className="truncate">{item.label}</span>
        </motion.button>
      );
    })}
  </AnimatePresence>
</div>
The width: 0width: "auto" animation requires no explicit pixel values — Framer Motion resolves the "auto" target at runtime by measuring the element’s natural dimensions after rendering.

Polaroid Hover Effect

The Polaroid photo cards on the /about page use motion.div with initial rotation and a whileHover override. Each card starts slightly tilted and snaps upright with a scale boost when hovered.
function Polaroid({ src, caption, rotation }) {
  return (
    <motion.div
      initial={{ rotate: rotation }}
      whileHover={{ scale: 1.1, rotate: 0, zIndex: 10 }}
      className="
        bg-white p-3 pb-8 shadow-xl border border-gray-200
        w-48 flex flex-col items-center cursor-pointer
      "
    >
      <img
        src={src}
        alt={caption}
        className="w-full h-40 object-cover border border-gray-300 pointer-events-none"
      />
      <span className="font-comic text-sm mt-3 text-gray-800">{caption}</span>
    </motion.div>
  );
}

{/* Usage — each photo gets a different tilt angle */}
<Polaroid
  src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=300"
  caption="Me coding (2025)"
  rotation={-5}
/>
<Polaroid
  src="https://images.unsplash.com/photo-1518770660439-4636190af475?w=300"
  caption="My CPU fan"
  rotation={3}
/>
Negative rotation values tilt the card counter-clockwise; positive values tilt clockwise. The whileHover variant overrides both rotate and scale simultaneously and releases them when the cursor leaves, snapping back to the initial tilt via Framer Motion’s spring exit.

Build docs developers (and LLMs) love