// OMNIX — Librería de animaciones estilo Aera Technology // Reveal on scroll · CountUp · Draw lines · Node diagrams // Todas sobrias, motivadas por física, máx. 600ms, sin decoración innecesaria. // ── useReveal ──────────────────────────────────────────────── // Retorna { ref, visible }. Fade + subida sutil al entrar viewport. // // FILOSOFÍA: el contenido NO puede quedar invisible por un bug del observer. // Default: visible=true. Sólo escondemos temporalmente si detectamos con // certeza que el elemento está fuera de viewport en el primer paint — // entonces animamos su entrada cuando entra. En cualquier caso de duda, // mostramos. const useReveal = (opts = {}) => { const ref = React.useRef(null); const [visible, setVisible] = React.useState(true); // ← default TRUE const armedRef = React.useRef(false); // ¿estamos animando entrada? // useLayoutEffect corre síncronamente antes del paint — leemos rect // y decidimos si queda fuera del viewport (armar animación) o no (dejar visible). React.useLayoutEffect(() => { if (!ref.current) return; const r = ref.current.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight || 800; // Si está claramente FUERA del viewport (top > vh + 100), armamos entrada if (r.top > vh + 100) { armedRef.current = true; setVisible(false); } }, []); React.useEffect(() => { if (!armedRef.current) return; // ya visible, nada que hacer if (!ref.current) return; let disposed = false; let raf = null; const check = () => { if (disposed || !ref.current) return; const r = ref.current.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight || 800; if (r.top < vh + 100) { setVisible(true); cleanup(); } }; const scheduleCheck = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; check(); }); }; const cleanup = () => { window.removeEventListener('scroll', scheduleCheck); window.removeEventListener('resize', scheduleCheck); if (raf) { cancelAnimationFrame(raf); raf = null; } }; window.addEventListener('scroll', scheduleCheck, { passive: true }); window.addEventListener('resize', scheduleCheck, { passive: true }); // Safety timer: si nada dispara scroll en 3s, mostrar de todas formas const safety = setTimeout(() => { setVisible(true); cleanup(); }, 3000); return () => { disposed = true; clearTimeout(safety); cleanup(); }; }, []); return { ref, visible }; }; // ── wrapper ────────────────────────────────────────── const Reveal = ({ children, delay = 0, y = 24, duration = 520, as: Tag = 'div', style = {}, ...rest }) => { const { ref, visible } = useReveal(); return ( {children} ); }; // ── — number counter tabular ───────────────────── const CountUp = ({ from = 0, to, prefix = '', suffix = '', duration = 1400, decimals = 0, style = {} }) => { const { ref, visible } = useReveal({ threshold: 0.35 }); const [value, setValue] = React.useState(from); React.useEffect(() => { if (!visible) return; const start = performance.now(); let raf; const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic setValue(from + (to - from) * eased); if (t < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [visible]); const formatted = value.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); return ( {prefix}{formatted}{suffix} ); }; // ── — SVG path que se dibuja al entrar viewport ──── const DrawLine = ({ d, stroke = '#1E1F26', strokeWidth = 1.5, duration = 1200, delay = 0, dashed = false, style = {} }) => { const { ref, visible } = useReveal({ threshold: 0.2 }); const pathRef = React.useRef(null); const [len, setLen] = React.useState(0); React.useEffect(() => { if (pathRef.current) setLen(pathRef.current.getTotalLength()); }, [d]); return ( ); }; // ── — nodo con pulse muy sutil ───────────────────── const PulseDot = ({ cx, cy, r = 5, color = '#F4024C', pulse = true }) => ( <> {pulse && ( )} ); // ── — punto que recorre un path ───────────── const FlowingParticle = ({ pathId, color = '#F4024C', duration = 3, size = 3 }) => ( ); // ── — fila auto-scroll infinita (logos) ─────────── const TickerRow = ({ children, speed = 40, style = {} }) => { const items = React.Children.toArray(children); return (
{items} {items} {items}
); }; // Inject marquee keyframes once if (typeof document !== 'undefined' && !document.getElementById('omnix-anim-keyframes')) { const s = document.createElement('style'); s.id = 'omnix-anim-keyframes'; s.textContent = ` @keyframes omnix-marquee { from { transform: translateX(0); } to { transform: translateX(-33.333%); } } @keyframes omnix-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } @keyframes omnix-flow-dash { to { stroke-dashoffset: -200; } } .omnix-blink { animation: omnix-blink 1.6s ease-in-out infinite; } .omnix-flow-dash { stroke-dasharray: 6 8; animation: omnix-flow-dash 3s linear infinite; } `; document.head.appendChild(s); } Object.assign(window, { useReveal, Reveal, CountUp, DrawLine, PulseDot, FlowingParticle, TickerRow });