Skip to main content

Documentation Index

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

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

The Home page is the first thing visitors see when they land on the portfolio. It sets the entire spooky tone with a large animated heading, a wry developer tagline, and a hand-crafted 3D scene built entirely from Tailwind utility classes and Framer Motion — no canvas, no images. The scene features a tombstone office building flanked by flickering pumpkin candles, a grinning pumpkin whose eyes silently track your cursor, and a row of tiny tombstone silhouettes lining the ground.

What It Displays

ElementDescription
Animated heading"Spooky Dev" in the haunt-moon teal color with a continuous oscillating rotation
Tagline"I write code that doesn't haunt your codebase. Usually."
Tombstone buildingDark rectangular building with a triangular roof clip and two vertical candle flames
Pumpkin faceOrange rounded element with two eye sockets whose pupils follow the mouse cursor
Ground silhouettesA row of 12 small tombstone shapes at 5 % random rotation offsets for a hand-planted look

Animated Hero Title

The "Spooky Dev" heading is wrapped in a Framer Motion <motion.span> with a looping rotation animation:
animate={{ rotate: [-2, 2, -2] }}
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
The entire heading block fades and rises on first load using the standard entrance animation shared across all pages:
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1 }}

Cursor-Tracking Pumpkin Eyes

The pumpkin’s pupils move to follow the visitor’s mouse pointer. The mechanic uses the mousePos value from the global useHaunt() hook and runs inside a useEffect that re-fires whenever mousePos changes.
const { mousePos } = useHaunt();

useEffect(() => {
  if (eyeRef.current) {
    const rect = eyeRef.current.getBoundingClientRect();
    const cx   = rect.left + rect.width  / 2;
    const cy   = rect.top  + rect.height / 2;

    const angle    = Math.atan2(mousePos.y - cy, mousePos.x - cx);
    const distance = Math.min(4, Math.hypot(mousePos.x - cx, mousePos.y - cy) / 10);

    setPupilOffset({
      x: Math.cos(angle) * distance,
      y: Math.sin(angle) * distance,
    });
  }
}, [mousePos]);
The resulting offset is applied as an inline transform on each pupil <div>:
style={{
  transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`
}}
Key parameters:
  • Max offset — pupils travel at most 4 px from center (Math.min(4, …)).
  • Distance scaling — raw pixel distance is divided by 10 before clamping, so the pupils start moving from very small cursor displacements.
  • Both eyes share the same offset state — they move in perfect sync.
The pumpkin eyes rely on mousePos from useHaunt(). When Haunt Mode is disabled, mousePos stops updating and the pupils freeze in their last position. This is expected behavior — toggling Haunt Mode back on immediately restores tracking.

The 3D Scene

The scene is a single relative-positioned <div> (max-w-3xl h-64) with border-b-4 border-haunt-dark forming the ground line. All child elements use absolute positioning within it.

Tombstone Building

<div className="relative w-48 h-56 bg-haunt-dark rounded-t-xl
                border-x-2 border-t-2 border-haunt-tombstoneDark">
  {/* Triangular roof */}
  <div className="absolute -top-16 w-56 h-24 bg-haunt-tombstoneDark"
       style={{ clipPath: 'polygon(50% 0%, 0% 100%, 100% 100%)' }} />

  {/* Two pumpkin candles */}
  <div className="flex gap-6 mb-8">
    <div className="w-8 h-12 bg-haunt-pumpkin/80 rounded-t-full animate-flicker
                    shadow-[0_0_20px_rgba(251,146,60,0.6)]" />
    <div className="w-8 h-12 bg-haunt-pumpkin/80 rounded-t-full animate-flicker
                    shadow-[0_0_20px_rgba(251,146,60,0.6)]"
         style={{ animationDelay: '0.5s' }} />
  </div>

  {/* Window */}
  <div className="w-12 h-20 bg-haunt-bg rounded-t-lg border-2 border-haunt-tombstoneDark" />
</div>

Ground Silhouettes

Twelve small tombstone shapes are generated with Array.from({ length: 12 }). Each is given a random slight tilt between −5° and +5° for a natural look:
{Array.from({ length: 12 }).map((_, i) => (
  <div
    key={i}
    className="w-2 h-full bg-haunt-tombstoneDark rounded-t-sm"
    style={{ transform: `rotate(${Math.random() * 10 - 5}deg)` }}
  />
))}

Customization Tips

Changing the Tagline

Open assets/main.js and locate the <p> element inside the hero <motion.div>:
<p className="text-xl md:text-2xl text-haunt-bone/80 font-body max-w-2xl mx-auto">
  I write code that doesn't haunt your codebase. Usually.
</p>
Replace the string literal with your preferred copy. Keep it under ~80 characters to avoid wrapping on mobile.

Adjusting the Tombstone Color

The building and ground silhouettes use the haunt-tombstone and haunt-tombstoneDark tokens defined in tailwind.config.js. Change the hex values there to update every tombstone-colored element across the whole portfolio at once:
// tailwind.config.js
theme: {
  extend: {
    colors: {
      haunt: {
        tombstone:     '#4b5563', // mid-grey by default
        tombstoneDark: '#374151', // darker border/accent
      }
    }
  }
}

Modifying the Candle Count

The two candle <div> elements inside the flex row are hard-coded. To add a third candle, duplicate one of the existing candle <div> nodes and optionally set a different animationDelay so they don’t all flicker in unison:
<div className="w-8 h-12 bg-haunt-pumpkin/80 rounded-t-full animate-flicker
                shadow-[0_0_20px_rgba(251,146,60,0.6)]"
     style={{ animationDelay: '1s' }} />

Changing the Oscillation Speed

Adjust the duration value on the heading’s Framer Motion transition. A smaller value makes the title rock faster; a larger value makes it sway slowly and lazily:
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
//                     ↑ change this value

Build docs developers (and LLMs) love