Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blairxu13/persona3-website/llms.txt

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

The portfolio uses two complementary animation layers that work at different points in the lifecycle of each page. CSS transitions and keyframes handle the initial entrance choreography of UI elements — things like menu items sliding in or bars sweeping across — while Framer Motion handles route-level orchestration, page wipes, and anything that needs tight keyframe timing control. Both layers rely heavily on clip-path polygon shapes to create the parallelogram and arrow silhouettes that define the game’s visual style.

CSS-based animations

The mounted-state pattern

Most interactive pages start with their elements hidden and off-screen, then set a mounted boolean to true inside a useEffect shortly after the component renders. CSS classes conditioned on mounted then trigger transitions so elements glide smoothly into their final positions. The small timeout ensures the browser has painted at least one frame with the initial (hidden) state before the transition fires.
// Pattern used in P3Menu, Socials, AboutMe, ResumePage
const [mounted, setMounted] = useState(false)

useEffect(() => {
  const t = setTimeout(() => setMounted(true), 1000) // P3Menu uses 1000ms
  return () => clearTimeout(t)
}, [])
In JSX the class is applied conditionally:
<a className={`p3-row ${mounted ? "mounted" : ""}`}>
  {/* menu item */}
</a>
And the CSS reacts to the class toggle:
/* Initial (unmounted) state */
.p3-row {
  opacity: 0;
  transform: translateX(36px);
  transition: opacity 0.38s ease, transform 0.38s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Final (mounted) state — triggered by class addition */
.p3-row.mounted {
  opacity: 1 !important;
  transform: translateX(0) !important;
}
Each item also receives a transitionDelay inline style (i * 80ms) so they cascade in one after another rather than all appearing simultaneously.
P3Menu uses a 1000 ms delay before setting mounted, which is deliberately longer than the other pages (Socials and ResumePage use 60–80 ms). This gives the looping background video and the rotated name-tag watermark time to settle before the menu items draw attention by animating in.

CSS keyframes catalogue

The following @keyframes rules are defined inline in each component’s <style> block:
NameComponentWhat it does
p3-shadow-popP3MenuThe triangular shadow behind the active menu item punches in with a scaleX overshoot: 0 → 1.22 → 0.96 → 1 over 0.28s
sc-reveal-bar-inAboutMeThe detail panel slides in from the left with a slight overshoot and a rotation of −20deg, animating translateX and scaleX together
sc-portrait-inAboutMeThe character portrait fades in from the right with a skew and a blur-to-sharp filter transition over 0.5s
resume-entry-revealResumePageA clip-path: circle() expands from 0 to 150vmax at the center of the screen, revealing a full-bleed video overlay
sc-right-nav-popSocials / AboutMeThe LB/RB navigation widget scales from 0.55 to 1.1 and back to 1, simulating a button press pop
sc-infobar-inSocialsRight-panel info bars slide in from translateX(40px) with an overshoot on each mount
sc-dim-inAboutMeA dark overlay fades from opacity: 0 to 1 when the reveal panel opens

Active-item highlight animation in P3Menu

When a new menu item becomes active, animKey is incremented to force a remount of the shadow triangle element, which re-triggers the p3-shadow-pop keyframe:
const activate = (idx) => {
  setActive(idx)
  setAnimKey(k => k + 1) // causes the shadow element to remount → animation replays
}
@keyframes p3-shadow-pop {
  0%   { transform: translateY(-40%) translateX(-12px) scaleX(0) scaleY(1); }
  55%  { transform: translateY(-46%) translateX(-15px) scaleX(1.22) scaleY(1.18); }
  75%  { transform: translateY(-39%) translateX(-11px) scaleX(0.96) scaleY(0.97); }
  100% { transform: translateY(-40%) translateX(-12px) scaleX(1) scaleY(1); }
}

.p3-shadow-tri.pop {
  animation: p3-shadow-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
The cubic-bezier(0.34, 1.56, 0.64, 1) spring easing produces the characteristic overshoot that makes the pop feel physical.

Clip-path shapes

Arrow shapes in P3Menu

Every menu item uses a triangular arrow clip-path generated by a small function that computes the polygon vertices from the element’s estimated width and height:
const CLIP_SHAPES = [
  (w, h) => `polygon(0px 0px, ${w}px ${h * 0.5}px, 0px ${h}px)`,
  (w, h) => `polygon(0px 0px, ${w}px ${h * 0.5}px, 0px ${h}px)`,
  (w, h) => `polygon(0px 0px, ${w}px ${h * 0.5}px, 0px ${h}px)`,
  (w, h) => `polygon(0px 0px, ${w}px ${h * 0.5}px, 0px ${h}px)`,
  (w, h) => `polygon(0px 0px, ${w}px ${h * 0.5}px, 0px ${h}px)`,
]
The three points describe a right-pointing triangle: top-left corner → midpoint of the right edge → bottom-left corner. The same function is applied to three layered elements on each item — the pink shadow triangle, the white highlight triangle, and the label’s clip-path — so all three share identical geometry and stack flush. Width and height are estimated from item.label.length * item.fontSize * 0.6 + 80 and item.fontSize * 0.94 respectively.

Parallelogram bars

The .sc-bar elements in Socials and AboutMe use a static clip-path that cuts a parallelogram with a slanted right edge:
.sc-bar {
  clip-path: polygon(0 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
}
The calc(100% - 14px) bottom-right point pulls the corner 14 pixels inward from the right edge, creating the characteristic shear on the right side. The same pattern at larger offsets (88px, 120px, 18px) appears on the detail panels in AboutMe and ResumePage, maintaining a consistent skewed-geometry visual language across pages. The character portrait inside each bar uses its own clip-path to punch out a centered parallelogram from the image:
.sc-char {
  clip-path: polygon(20px 0%, 100% 0%, calc(100% - 20px) 100%, 0% 100%);
}

Framer Motion usage

Keyframe arrays and the times prop

Framer Motion accepts arrays in the animate prop to define multi-step keyframe sequences on any property. The times array (values 0–1) maps each keyframe to a fraction of the total duration:
// DefaultTransition panel — expand then collapse
<motion.div
  initial={{ scaleX: 0 }}
  animate={{ scaleX: [0, 1, 1, 0] }}
  transition={{
    duration: 0.45,
    delay: i * 0.05,
    times: [0, 0.4, 0.6, 1],   // keyframe at 0ms, 180ms, 270ms, 450ms
    ease: [0.76, 0, 0.24, 1],  // cubic-bezier — fast out, slow settle
  }}
/>
This pattern (expand → hold → collapse) is reused across all four transition variants with different properties (scaleX, x, y) and different timing ratios.

AnimatePresence and route transitions

AnimatePresence mode="wait" is used at two levels:
  1. In App.jsx — wraps <Routes> so that the exiting route’s exit animation completes before the new route mounts.
  2. Inside PageTransition — wraps the content motion.div with a simple opacity: 0 exit so the page fades out cleanly if AnimatePresence triggers its exit.
// App.jsx — top-level route orchestration
<AnimatePresence mode="wait">
  <Routes location={location} key={location.pathname}>
    {/* routes */}
  </Routes>
</AnimatePresence>

// PageTransition.jsx — per-page content fade
<AnimatePresence mode="wait">
  <motion.div key={location.pathname}>
    <TransitionOverlay variant={variant} />
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.2, delay: 0.18 }}
    >
      {children}
    </motion.div>
  </motion.div>
</AnimatePresence>
The mode="wait" setting is important at the top level — without it, the incoming and outgoing route components would both be in the DOM simultaneously, and the wipe panels from the new route’s PageTransition would render on top of a still-visible old page.
The p3-shadow-pop CSS animation and the Framer Motion panel keyframes both use spring-flavored cubic-bezier curves ([0.34, 1.56, 0.64, 1] and [0.76, 0, 0.24, 1] respectively). These are not identical — the shadow pop uses an overshoot curve while the panel wipes use an anticipation-then-fast curve — chosen to match the weight and speed of each element type.

Build docs developers (and LLMs) love