// sections.jsx — content overlays for each section.
// The 3D art is rendered by three-scene.js as a global background; these
// components only draw the text/buttons/lists that sit on top.
const { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo } = React;
// ── Emit gold sparks at a screen point ──────────────────────
function emitSparks(x, y, count = 8) {
const layer = document.getElementById('sparks');
if (!layer) return;
for (let i = 0; i < count; i++) {
const s = document.createElement('div');
s.className = 'spark';
s.style.left = x + 'px';
s.style.top = y + 'px';
const angle = Math.random() * Math.PI * 2;
const dist = 40 + Math.random() * 80;
s.style.setProperty('--dx', Math.cos(angle) * dist + 'px');
s.style.setProperty('--dy', Math.sin(angle) * dist + 'px');
layer.appendChild(s);
setTimeout(() => s.remove(), 850);
}
}
window.emitSparks = emitSparks;
// ── Glitch / scramble reveal text ───────────────────────────
function GlitchText({ text, className, style, delay = 0, duration = 700, active = true }) {
const [out, setOut] = useState('');
useEffect(() => {
if (!active) {setOut(text);return;}
const chars = '!<>-_\\/[]{}—=+*^?#█▓▒░·';
let start = performance.now() + delay;
let raf;
const tick = (now) => {
const p = Math.max(0, Math.min(1, (now - start) / duration));
if (p <= 0) {setOut('');raf = requestAnimationFrame(tick);return;}
let s = '';
for (let i = 0; i < text.length; i++) {
const cp = i / text.length;
if (p > cp + 0.05) s += text[i];else
if (p > cp - 0.05) s += chars[Math.floor(Math.random() * chars.length)];else
s += ' ';
}
setOut(s);
if (p < 1) raf = requestAnimationFrame(tick);else
setOut(text);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [text, delay, duration, active]);
return {out || '\u00A0'};
}
// ── Hover/click spark wrapper ───────────────────────────────
function Sparkly({ children }) {
const onClick = (e) => {
emitSparks(e.clientX, e.clientY, 14);
if (window.__pulseFlash) window.__pulseFlash();
if (children.props.onClick) children.props.onClick(e);
};
return React.cloneElement(children, { onClick });
}
// ── Magnetic button (subtle pull toward cursor) ─────────────
function Magnetic({ children, strength = 14 }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
let raf;
let tx = 0,ty = 0,cx = 0,cy = 0;
const onMove = (e) => {
const r = el.getBoundingClientRect();
const cxR = r.left + r.width / 2;
const cyR = r.top + r.height / 2;
const dist = Math.hypot(e.clientX - cxR, e.clientY - cyR);
if (dist < 160) {
const k = (1 - dist / 160) * strength;
tx = (e.clientX - cxR) / r.width * k;
ty = (e.clientY - cyR) / r.height * k;
} else {
tx = 0;ty = 0;
}
};
const onLeave = () => {tx = 0;ty = 0;};
const loop = () => {
cx += (tx - cx) * 0.18;
cy += (ty - cy) * 0.18;
el.style.transform = `translate3d(${cx}px, ${cy}px, 0)`;
raf = requestAnimationFrame(loop);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerleave', onLeave);
loop();
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerleave', onLeave);
};
}, [strength]);
return
{children}
;
}
// ── Live ticker stats ───────────────────────────────────────
function useTicker(interval = 50) {
const [n, setN] = useState(0);
useEffect(() => {
const t = setInterval(() => setN((x) => x + 1), interval);
return () => clearInterval(t);
}, [interval]);
return n;
}
// ── Hero ↔ About transition phase ───────────────────────────
// Subscribes to the global `hxtransition` event dispatched by three-scene.js.
// Returns `{ phase, variant }` where phase is:
// 'active' — transition is running (3D effect + content animating)
// 'settling' — 3D done, give about's entry animation a beat to complete
// null — idle
function useTransitionPhase() {
const [data, setData] = useState({ phase: null, variant: null });
useEffect(() => {
let settleTimer;
const onT = (e) => {
const { phase, variant } = e.detail || {};
if (phase === 'start') {
clearTimeout(settleTimer);
setData({ phase: 'active', variant });
} else if (phase === 'end') {
setData((d) => ({ phase: 'settling', variant: d.variant || variant }));
clearTimeout(settleTimer);
settleTimer = setTimeout(() => setData({ phase: null, variant: null }), 700);
}
};
window.addEventListener('hxtransition', onT);
return () => {
window.removeEventListener('hxtransition', onT);
clearTimeout(settleTimer);
};
}, []);
return data;
}
// ═══════════════════════════════════════════════════════════
// 01 — HERO
// ═══════════════════════════════════════════════════════════
function Hero({ active }) {
const { phase: tPhase, variant: tVariant } = useTransitionPhase();
const exiting = !!tPhase; // active or settling — keep exited until idle
return (
// INICIOhx.node.01
hecstrim
Hector. Creador de contenido, constructor de agentes.
IA, automatización y ruido digital con propósito.
scroll ↓ · acerca el cursor
loop activo · ↻
lat 40.4168 · lon −3.7038
render · 60 fps · webgl2
);
}
// ═══════════════════════════════════════════════════════════
// HectorCard — 3D vertical card body with real Y-axis rotation.
// The card has front face (photo), back face (data), and four thin
// edge slabs that give the body genuine thickness. Three variants:
// 'showcase' → continuous slow 360° Y-spin, museum display feel
// 'reveal' → step-rotate: 180° flip, pause, 180° flip, pause
// 'magnet' → idle slow spin + cursor X pushes rotation speed
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
// HectorCard — 3D vertical card body. Front face always faces the
// viewer with a gentle Y-axis sway, biased by cursor position
// (pointer adds yaw + slight pitch). Yaw never exceeds ~±30°.
// ═══════════════════════════════════════════════════════════
function HectorCard() {
const wrapRef = useRef(null);
const cardRef = useRef(null); // the rotating body
const shadowRef = useRef(null); // ground shadow
// Card sizing — kept identical across variants
const CARD_W = 'min(340px, 78%)';
const CARD_AR = '5 / 7';
const THICK = 12; // px, half-depth of the card body (full thickness = 24px)
// Ground shadow: breathes with the float cycle (used by all variants)
useEffect(() => {
const shadow = shadowRef.current;
if (!shadow) return;
let raf;
const start = performance.now();
const period = 5200;
const tick = () => {
const t = (performance.now() - start) / period;
const lift = Math.sin(t * Math.PI * 2);
const s = 1 + lift * 0.12;
const o = 0.42 + lift * 0.18;
shadow.style.transform = `translateX(-50%) scale(${s.toFixed(3)}, 1)`;
shadow.style.opacity = o.toFixed(3);
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
// Idle Y-axis sway + cursor parallax. Front face stays visible at all times
// (yaw stays well within ±30°). Cursor X biases the yaw; cursor Y biases X-tilt.
useEffect(() => {
const wrap = wrapRef.current;
const card = cardRef.current;
if (!wrap || !card) return;
let targetYawBias = 0; // deg, from cursor X
let targetPitch = 0; // deg, from cursor Y
let yawBias = 0,pitch = 0;
let raf;
const t0 = performance.now();
const onMove = (e) => {
const r = wrap.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
// Normalize cursor distance from card center to viewport scale
const nx = (e.clientX - cx) / (window.innerWidth * 0.5);
const ny = (e.clientY - cy) / (window.innerHeight * 0.5);
// Cap effect
targetYawBias = Math.max(-1, Math.min(1, nx)) * 18; // ± 18°
targetPitch = Math.max(-1, Math.min(1, ny)) * -10; // ± 10° (inverted)
};
const onLeave = () => {targetYawBias = 0;targetPitch = 0;};
const tick = (now) => {
const elapsed = (now - t0) / 1000;
// Idle yaw oscillation: ± 12°, 6 s period
const idleYaw = Math.sin(elapsed * (Math.PI * 2) / 6) * 12;
// Smooth toward cursor targets
yawBias += (targetYawBias - yawBias) * 0.06;
pitch += (targetPitch - pitch) * 0.06;
const yaw = idleYaw + yawBias;
card.style.transform = `rotateY(${yaw.toFixed(2)}deg) rotateX(${pitch.toFixed(2)}deg)`;
raf = requestAnimationFrame(tick);
};
window.addEventListener('pointermove', onMove);
window.addEventListener('pointerleave', onLeave);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerleave', onLeave);
};
}, []);
// Inline keyframes for the vertical float bob
const KEYFRAMES = `
@keyframes hxFloatA {
0%, 100% { transform: translateY(-6px); }
50% { transform: translateY(8px); }
}
`;
// Corner radius applied to all six surfaces of the body.
const RADIUS = 24;
// The card body. Front face holds the photo (framed with margin), back face
// holds the persona data, and four edge slabs give the body real thickness.
// Edge slabs are inset from the rounded corners so they don't poke past them.
const renderBody = () =>
<>
{/* FRONT face — photo framed by margin, rounded corners */}
{/* image area — inset from the card edge with its own rounded corners */}
{/* scanlines */}
{/* header overlay on top of image */}
{/* footer — sits in the frame margin below the image */}
PERSONAJE
· hecstrim
Ingeniero IA · Creador de contenido
{/* BACK face — closes the body, gray surface matching the edge slabs.
Reserved for the personal brand logo (to come). */}
{/* EDGE slabs — inset from rounded corners so they don't poke out */}
{/* top edge */}
{/* bottom edge */}
{/* left edge */}
{/* right edge */}
>;
// The wrapper sits inside the section column; everything sits in a perspective stage.
return (
{/* ground shadow */}
{/* outer floater: handles the vertical bob */}
{/* inner rotator: the actual 3D body */}
{renderBody()}
);
}
// ═══════════════════════════════════════════════════════════
// 02 — ABOUT
// ═══════════════════════════════════════════════════════════
function About({ active }) {
const { phase: tPhase, variant: tVariant } = useTransitionPhase();
// Two-step entry. The transition fires the SAME tick that .hx-transitioning
// pins About as a fixed full-viewport overlay — so if About paints at its
// settled state for even one frame, the user sees the content flash into the
// center BEFORE it snaps to the displaced start state. That's the
// "bajan-suben-bajan" glitch.
//
// Fix: useLayoutEffect runs AFTER React commits but BEFORE the browser
// paints. setState inside it triggers a synchronous re-render that is
// flushed before paint, so the very first paint already shows About at
// hx-enter-start (translateY 60px, opacity 0, snapped via
// transition-duration:0s). No center flash, no extra "down" movement.
//
// Then useEffect (post-paint) schedules a 2-rAF beat to flip to hx-enter so
// the CSS transition fires and About slides up to its settled position once,
// smoothly.
const [enterClass, setEnterClass] = useState('');
useLayoutEffect(() => {
if (tPhase === 'active') {
setEnterClass('hx-enter-start');
} else if (tPhase === null) {
setEnterClass('');
}
// 'settling' — leave enterClass at 'hx-enter' so it stays settled
}, [tPhase]);
useEffect(() => {
if (enterClass !== 'hx-enter-start') return;
// Hold About at the displaced/invisible state until AFTER the 3D warp
// (1.6s) fully resolves, so the dive does the visual heavy lifting
// alone and the content arrives as a follow-up beat — not concurrent.
const ENTER_HOLD_MS = 1800;
const timer = setTimeout(() => setEnterClass('hx-enter'), ENTER_HOLD_MS);
return () => clearTimeout(timer);
}, [enterClass]);
const stats = [
['focus.areas', 'IA · Automation · Storytelling'],
['based.in', 'Madrid · everywhere online'],
['stack', 'LLMs · Python · Next · Three'],
['status', 'shipping daily · open to collab']];
return (
//PERFIL
{/* Left — title spans into the 3D portrait area */}
Este es HECTOR.
Ingeniero y maker de IA que ayuda a otras personas en su camino con la IA, a través de materiales, formación en inteligencia artificial y tecnología.