/* Billboard AR Experience — UI kit FX (hand-ported, no deps)
Premium motion inspired by 21st.dev (shaders + tilt), implemented in
plain WebGL + React. Everything degrades gracefully and respects
prefers-reduced-motion. Loaded BEFORE the section files so
and are available as globals (same pattern as Reveal/Section). */
const { useEffect: useFxEffect, useRef: useFxRef, useState: useFxState } = React;
const __fxReduce = () =>
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ============================================================
ShaderBG — flowing azul→violeta→magenta gradient (WebGL).
Sits behind the hero content, blended over the photo. If WebGL
is unavailable the canvas simply renders nothing and the CSS
bloom underneath carries the look.
============================================================ */
function ShaderBG({ opacity = 0.9, style }) {
const ref = useFxRef(null);
useFxEffect(() => {
const canvas = ref.current;
if (!canvas) return;
const gl = canvas.getContext("webgl", { antialias: true, alpha: true, premultipliedAlpha: false });
if (!gl) return;
const VERT = "attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }";
const FRAG = [
"precision highp float;",
"uniform vec2 u_res; uniform float u_t;",
"vec3 BLUE = vec3(0.176, 0.357, 1.000);",
"vec3 VIOL = vec3(0.545, 0.239, 1.000);",
"vec3 MAG = vec3(0.878, 0.129, 0.541);",
"vec3 CORAL = vec3(1.000, 0.302, 0.427);",
"float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }",
"float noise(vec2 p){",
" vec2 i = floor(p), f = fract(p);",
" vec2 u = f * f * (3.0 - 2.0 * f);",
" return mix(mix(hash(i + vec2(0.0,0.0)), hash(i + vec2(1.0,0.0)), u.x),",
" mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), u.x), u.y);",
"}",
"float fbm(vec2 p){ float v = 0.0, a = 0.5; for(int i = 0; i < 5; i++){ v += a * noise(p); p *= 2.0; a *= 0.5; } return v; }",
"void main(){",
" vec2 uv = gl_FragCoord.xy / u_res.xy;",
" vec2 q = uv; q.x *= u_res.x / u_res.y;",
" float t = u_t * 0.05;",
" float n = fbm(q * 2.2 + vec2(t, t * 0.6));",
" float m = fbm(q * 1.3 - vec2(t * 0.4, t));",
" float g = uv.y * 0.55 + n * 0.85 + m * 0.45;",
" vec3 col = mix(BLUE, VIOL, smoothstep(0.10, 0.62, g));",
" col = mix(col, MAG, smoothstep(0.45, 0.98, g));",
" col = mix(col, CORAL, smoothstep(0.82, 1.20, g) * 0.45);",
" float bloom = smoothstep(1.0, 0.0, uv.y);", // brightest at the bottom
" col *= 0.50 + 0.95 * bloom;",
" float alpha = 0.30 + 0.70 * bloom;",
" gl_FragColor = vec4(col, alpha);",
"}"
].join("\n");
const compile = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
};
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return;
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "p");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const uRes = gl.getUniformLocation(prog, "u_res");
const uT = gl.getUniformLocation(prog, "u_t");
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const resize = () => {
const w = canvas.clientWidth, h = canvas.clientHeight;
if (!w || !h) return;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
gl.viewport(0, 0, canvas.width, canvas.height);
};
const draw = (t) => {
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uT, t);
gl.drawArrays(gl.TRIANGLES, 0, 3);
};
let raf = null, start = null;
resize();
window.addEventListener("resize", resize);
if (__fxReduce()) {
draw(8.0); // a single, pleasant static frame
} else {
const loop = (ts) => {
if (start === null) start = ts;
draw((ts - start) / 1000);
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
}
return () => {
if (raf) cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
};
}, []);
return (
);
}
/* ============================================================
Tilt — pointer-driven 3D tilt + accent glow wrapper.
Wraps any card; composes with the card's own hover state.
No-op (flat) under reduced-motion or on coarse pointers.
============================================================ */
function Tilt({ children, max = 7, style, className, ...rest }) {
const ref = useFxRef(null);
useFxEffect(() => {
const el = ref.current;
if (!el || __fxReduce()) return;
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
let raf = null;
const move = (e) => {
const r = el.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width;
const py = (e.clientY - r.top) / r.height;
const rx = (0.5 - py) * max * 2;
const ry = (px - 0.5) * max * 2;
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
el.style.transform = "perspective(820px) rotateX(" + rx.toFixed(2) + "deg) rotateY(" + ry.toFixed(2) + "deg)";
});
};
const leave = () => {
if (raf) cancelAnimationFrame(raf);
el.style.transform = "";
};
el.addEventListener("pointermove", move);
el.addEventListener("pointerleave", leave);
return () => {
el.removeEventListener("pointermove", move);
el.removeEventListener("pointerleave", leave);
};
}, []);
return (
{children}
);
}
/* ============================================================
DoorReveal — a pair of doors that swing open in 3D to reveal
the children, on scroll into view. Built for the "la marca que
te abre la puerta" section. No-JS / reduced-motion → starts open.
============================================================ */
function DoorReveal({ children, style }) {
const ref = useFxRef(null);
const [open, setOpen] = useFxState(false);
useFxEffect(() => {
const el = ref.current;
if (!el) return;
if (__fxReduce() || !("IntersectionObserver" in window)) { setOpen(true); return; }
const io = new IntersectionObserver(
(entries) => entries.forEach((e) => { if (e.isIntersecting) { setOpen(true); io.disconnect(); } }),
{ threshold: 0.4 }
);
io.observe(el);
return () => io.disconnect();
}, []);
const door = (isLeft) => ({
position: "absolute", top: 0, bottom: 0, width: "50%",
[isLeft ? "left" : "right"]: 0,
transformOrigin: isLeft ? "left center" : "right center",
transform: open ? "rotateY(" + (isLeft ? -115 : 115) + "deg)" : "rotateY(0deg)",
transition: "transform 1.5s cubic-bezier(0.77, 0, 0.18, 1)",
backfaceVisibility: "hidden",
background: "linear-gradient(135deg, #0B1020 0%, #07080F 72%)",
boxShadow: open ? "none" : "0 40px 90px -20px rgba(0,0,0,0.55)",
borderRadius: isLeft ? "var(--radius-lg) 0 0 var(--radius-lg)" : "0 var(--radius-lg) var(--radius-lg) 0",
pointerEvents: open ? "none" : "auto",
overflow: "hidden",
willChange: "transform",
});
const Door = (isLeft) => (
{/* sheen from the top */}
{/* center seam accent (inner edge where both doors meet) */}
{/* handle */}
);
return (
{children}
{Door(true)}
{Door(false)}
);
}
Object.assign(window, { ShaderBG, Tilt, DoorReveal });