Skip to main content

Documentation Index

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

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

Player One’s animation system operates on two independent layers. The first is Framer Motion — a React animation library that handles orchestrated JS-driven transitions: page entrances and exits, floating ghost sprites, and interactive hover lifts. The second is CSS keyframes — lightweight, GPU-accelerated loops that run entirely off the main thread and handle the perpetual ambient effects: CRT flicker, scanline overlays, and the glitch text corruption. Together, the two layers create a display that feels alive without sacrificing performance.

Page Transition

Every route change is wrapped in a PageTransition component that blurs and scales the outgoing page out, then blurs and scales the incoming page in. The simultaneous scale and blur change creates the impression of a screen powering on — as if the new stage is snapping into focus from an out-of-sync signal.
<motion.div
  initial={{ opacity: 0, scale: 0.95, filter: 'blur(10px)' }}
  animate={{ opacity: 1, scale: 1,    filter: 'blur(0px)'  }}
  exit={{    opacity: 0, scale: 1.05, filter: 'blur(10px)' }}
  transition={{ duration: 0.4, ease: 'easeOut' }}
>
  {children}
</motion.div>
Stateopacityscalefilter
initial (enter start)00.95blur(10px)
animate (settled)11blur(0px)
exit (leave end)01.05blur(10px)
The exit scale is slightly larger than 1 (1.05) so the outgoing page appears to push away rather than simply fade — a subtle depth cue that distinguishes a forward navigation from a backward one.

Ghost Sprite Animation

A GhostSprite component drifts a pixel-art ghost across the full viewport width at a randomized height, repeating forever. The traversal and vertical oscillation are driven by two separate Framer Motion animate calls composed together.
<motion.div
  animate={{
    x: ['-10vw', '110vw'],                       // traverse full screen left → right
    y: [startY, startY - 100, startY + 50, startY], // oscillate vertically around spawn height
  }}
  transition={{
    x: {
      duration: 12,
      ease: 'linear',
      repeat: Infinity,
    },
    y: {
      duration: duration / 2,
      ease: 'easeInOut',
      repeat: Infinity,
      repeatType: 'mirror',
    },
  }}
/>
x
string[]
Traversal keyframes. Starts at -10vw so the sprite enters from fully off-screen left; ends at 110vw so it exits fully off-screen right before looping.
y
number[]
Vertical oscillation keyframes in pixels, expressed relative to startY (the sprite’s randomized spawn height). The mirror repeat type reverses the sequence on alternate iterations, producing a smooth floating bob rather than a hard reset.
duration (x)
number
12 seconds per full traversal. Adjust to control ghost speed — lower values produce a faster, more frantic sprite.
duration (y)
number
duration / 2 — half the sprite’s total traversal duration per oscillation cycle. Runs independently of x so the vertical phase drifts relative to the horizontal position on each pass.

CRT Flicker

The .crt-overlay class drives a continuous 0.15 s opacity loop that simulates the natural luminance instability of a cathode-ray tube operating at the edge of its refresh cycle.
@keyframes flicker {
  0%   { opacity: 0.95; }
  5%   { opacity: 0.85; }
  10%  { opacity: 0.95; }
  15%  { opacity: 1.00; }
  100% { opacity: 1.00; }
}
The asymmetric keyframe curve — a brief dip to 0.85 at 5 % followed by a recovery and a hold at full opacity — produces an irregular flutter rather than a uniform pulse. This matches the characteristic one-sided flicker pattern of fluorescent-phosphor displays rather than a simple sine-wave strobe.
The flicker animation is applied to .crt-overlay, which also carries the scanline and color-fringing background layers. Animating a single overlay element keeps the effect to one composite layer, avoiding the GPU cost of independently animating every element on the page.

Glitch Animation

The glitch effect uses two @keyframes sequences that move a clip rectangle across the element’s height at different speeds and offsets, making it look as if horizontal strips of the text are being misread from corrupted video memory.
@keyframes glitch-anim {
  0%   { clip: rect(24px, 550px, 90px,  0); transform: skew(0.5deg);  }
  20%  { clip: rect(62px, 550px, 95px,  0); transform: skew(0.2deg);  }
  40%  { clip: rect(8px,  550px, 110px, 0); transform: skew(0.8deg);  }
  60%  { clip: rect(78px, 550px, 100px, 0); transform: skew(0.4deg);  }
  80%  { clip: rect(45px, 550px, 80px,  0); transform: skew(0.1deg);  }
  /* ... continues sweeping through the full element height ... */
  100% { clip: rect(24px, 550px, 90px,  0); transform: skew(0.5deg);  }
}

@keyframes glitch-anim-2 {
  0%   { clip: rect(65px, 550px, 119px, 0); transform: skew(0.3deg);  }
  20%  { clip: rect(15px, 550px, 70px,  0); transform: skew(0.6deg);  }
  40%  { clip: rect(37px, 550px, 84px,  0); transform: skew(0.9deg);  }
  /* ... continues through 60 %, 80 % with independent clip offsets ... */
  100% { clip: rect(65px, 550px, 119px, 0); transform: skew(0.3deg);  }
}
The ::before pseudo-element runs glitch-anim-2 at 3 s with a #ff00aa (magenta) text shadow, and ::after runs glitch-anim at 2.5 s with a #00ffff (cyan) text shadow. Because the two durations are not integer multiples of each other, the two corruption layers never fall into a repeating visual pattern, keeping the glitch looking genuinely random.
{/* The data-text value must match visible text exactly */}
<h1
  className="glitch font-pixel text-arcade-lime text-4xl md:text-6xl"
  data-text="PLAYER ONE"
>
  PLAYER ONE
</h1>
The data-text attribute must exactly match the element’s visible text content. Both pseudo-elements use content: attr(data-text) to clone the text — any mismatch causes the ghost layers to display different characters than the base text, breaking the corruption illusion.

Hover Effects

Player One layers three distinct hover interactions depending on the element type.

ArcadeCabinet Card Lift

Project cards use Framer Motion’s whileHover to lift 10 px upward on hover, simulating the physical push of an arcade button being raised:
<motion.div
  whileHover={{ y: -10 }}
  transition={{ type: 'spring', stiffness: 300, damping: 20 }}
  className="bg-arcade-dark border border-arcade-lime"
>
  {/* project card content */}
</motion.div>

Button Color Transitions

Interactive buttons and links use a CSS transition at 150 ms — short enough to feel instant but long enough to avoid a harsh snap:
<button className="
  font-pixel text-arcade-black bg-arcade-lime
  hover:bg-arcade-cyan
  transition-colors duration-150
">
  PLAY
</button>

Box Glow on Hover

Bordered panels and nav items use the .hover:box-glow-* utility classes to activate the inset neon glow when focused or hovered:
<div className="border border-arcade-lime hover:box-glow-lime   transition-shadow duration-150">STAGE 1</div>
<div className="border border-arcade-magenta hover:box-glow-magenta transition-shadow duration-150">STAGE 2</div>
<div className="border border-arcade-cyan hover:box-glow-cyan   transition-shadow duration-150">STAGE 3</div>
The full-screen navigation overlay mounts and unmounts through Framer Motion’s AnimatePresence, which keeps the exit animation alive until it completes before removing the element from the DOM.
import { AnimatePresence, motion } from 'framer-motion'

<AnimatePresence>
  {isOpen && (
    <motion.div
      key="nav-modal"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{    opacity: 0 }}
      transition={{ duration: 0.25, ease: 'easeInOut' }}
      className="fixed inset-0 bg-arcade-black z-50"
    >
      {/* navigation links */}
    </motion.div>
  )}
</AnimatePresence>
AnimatePresence is required any time you need a Framer Motion component to animate on exit. Without it, React unmounts the component immediately on a state change and the exit variant never runs. The Navigation modal relies on AnimatePresence so the opacity fade-out completes before the overlay is removed from the DOM — preventing a jarring instant disappearance.

Build docs developers (and LLMs) love