diff --git a/portfolio-view/src/components/hero/HeroSection.tsx b/portfolio-view/src/components/hero/HeroSection.tsx index 1bae505..29bb903 100644 --- a/portfolio-view/src/components/hero/HeroSection.tsx +++ b/portfolio-view/src/components/hero/HeroSection.tsx @@ -1,18 +1,23 @@ import { useEffect, useState } from 'react' /** - * nameStage: - * 0 – all hidden, b/vic.com reset to off-screen (transition: none) - * 1 – "Alex" fades in, b/vic.com still off-screen - * 2 – b slides in from left, vic.com from right; "A" → "a" - * 3 – b/vic.com fade out in-place; "a" → "A" + * Cycle timeline (all times relative to stage-1 firing): + * +0ms stage 1 — Alex fades in (alexOn=true, nameStage=1) + * +1300ms stage 2 — b/vic.com start sliding in; A→a crossfade begins + * +4300ms shift — slides done (~3 s), heading drifts left over 3 s + * +7300ms stage 3 — shift done; b/vic.com fade out; a→A; heading drifts back + * +9400ms alexOff — b/vic.com nearly gone (2.1 s); Alex starts fading (2.4 s) + * +11800ms reset — Alex gone; hard-reset b/vic.com positions (transition:none) + * 5 000ms pause + * +16800ms — next stage 1 → CYCLE_GAP = 16 800 ms * - * Cycle: 0→1 (300ms) →2 (1600ms) →3 (4600ms) →0 (7400ms) → repeat every 7600ms - * Content (subtitle, buttons, etc.) fades in once at 3000ms and stays. + * Content (badge, role, desc, buttons, scroll) fades up once at t=3 s, stays forever. */ export function HeroSection() { - const [nameStage, setNameStage] = useState(0) + const [nameStage, setNameStage] = useState(0) + const [alexOn, setAlexOn] = useState(false) + const [shiftLeft, setShiftLeft] = useState(false) const [contentVisible, setContentVisible] = useState(false) useEffect(() => { @@ -21,45 +26,48 @@ export function HeroSection() { const add = (fn: () => void, ms: number) => timers.push(setTimeout(() => { if (alive) fn() }, ms)) - const CYCLE = 7600 + const CYCLE_GAP = 16800 - const scheduleCycle = (offset: number) => { - add(() => setNameStage(1), offset + 300) - add(() => setNameStage(2), offset + 1600) - add(() => setNameStage(3), offset + 4600) - add(() => setNameStage(0), offset + 7400) + const scheduleCycle = (t1: number) => { + add(() => { setNameStage(1); setAlexOn(true) }, t1) + add(() => setNameStage(2), t1 + 1300) + add(() => setShiftLeft(true), t1 + 4300) + add(() => { setNameStage(3); setShiftLeft(false) }, t1 + 7300) + add(() => setAlexOn(false), t1 + 9400) + add(() => setNameStage(0), t1 + 11800) } - scheduleCycle(0) + scheduleCycle(300) add(() => setContentVisible(true), 3000) - for (let i = 1; i <= 40; i++) scheduleCycle(CYCLE * i) + for (let i = 1; i <= 20; i++) scheduleCycle(300 + CYCLE_GAP * i) return () => { alive = false; timers.forEach(clearTimeout) } }, []) - /** Styles for the sliding "b" and "vic.com" spans */ + /* ── style helpers ── */ + + const noTrans = nameStage === 0 + + /** Sliding accent spans ("b" left, "vic.com" right) */ const sideStyle = (side: 'left' | 'right'): React.CSSProperties => { const offscreen = side === 'left' ? 'translateX(-110vw)' : 'translateX(110vw)' - const reset = nameStage === 0 - const visible = nameStage === 2 - const fadingOut = nameStage === 3 + const active = nameStage === 2 + const fadeOut = nameStage === 3 return { - display: 'inline-block', color: '#22d3ee', - opacity: visible ? 1 : 0, - transform: visible || fadingOut ? 'translateX(0)' : offscreen, - transition: reset - ? 'none' - : 'opacity 0.7s ease-out, transform 1s ease-out', + opacity: active ? 1 : 0, + transform: active || fadeOut ? 'translateX(0)' : offscreen, + transition: noTrans ? 'none' : 'opacity 2.1s ease-out, transform 3s ease-out', + whiteSpace: 'nowrap', } } - /** Generic fade-up for content blocks */ + /** Content blocks below the heading */ const fadeUp = (active: boolean, delay = '0s'): React.CSSProperties => ({ opacity: active ? 1 : 0, transform: active ? 'translateY(0)' : 'translateY(18px)', - transition: `opacity 0.8s ease ${delay}, transform 0.8s ease ${delay}`, + transition: `opacity 2.4s ease ${delay}, transform 2.4s ease ${delay}`, }) return ( @@ -90,35 +98,83 @@ export function HeroSection() { - {/* ── Animated heading ── */} -

- {/* "b" — slides in from the left */} - b - - {/* "Alex" / "alex" — fades in first */} - = 1 ? 1 : 0, - transition: nameStage === 0 ? 'none' : 'opacity 0.8s ease-out', + display: 'grid', + gridTemplateColumns: 'minmax(0,1fr) auto minmax(0,1fr)', + alignItems: 'baseline', + fontSize: 'clamp(3rem, 9.5vw, 7.5rem)', + letterSpacing: '-0.02em', + whiteSpace: 'nowrap', + transform: 'scaleX(0.84)', + transformOrigin: 'center center', }} > - {nameStage === 2 ? 'alex' : 'Alex'} - - {/* "vic.com" — slides in from the right */} - vic.com -

+ {/* "b" — right-aligned so it sits flush against "Alex" */} + b + + {/* "Alex" / "alex" */} + + {/* + * A/a crossfade: "A" lives in normal flow (sets the container width), + * "a" is absolutely overlaid and centered over "A". + * Both fade over 3.5 s (5× the base 0.7 s) — nameStage===2 means + * "b" has arrived, so lowercase "a" should be visible. + */} + + + A + + + a + + + lex + + + {/* "vic.com" — left-aligned so it sits flush against "Alex" */} + vic.com + + + {/* Role */}
@@ -128,7 +184,7 @@ export function HeroSection() {
{/* Description */} -
+

Building backend systems and intelligent applications with{' '} Spring Boot,{' '} @@ -138,7 +194,7 @@ export function HeroSection() {

{/* CTA buttons */} -
+
{/* Scroll hint */} -
+