Hero reveal con AI

El efecto del hero de este sitio: por fuera mi foto real, y bajo el cursor se asoma mi modo AI-native dentro de un círculo de luz. Dos imágenes gemelas de ChatGPT, un script de 10 líneas y CSS que corre en GPU.

Workflow activo2 imágenes gemelas30 minutoscero librerías

La demo

Wen López, foto base del hero
Las dos gemelas reales del sitio. Mueve el cursor (o el dedo) sobre la foto.

El sistema

Imágenes
ChatGPTreveal con identity lock + gemela base
Alineación
Python + Pillowresize + crop, corrige el 5% de desfase
Efecto
CSS clip-pathcírculo que recorta la capa AI
Lerp del cursorrequestAnimationFrame, suavizado 0.14
Resultado
El hero de este sitiobase visible, modo AI bajo el cursor

Las herramientas

ChatGPTimágenes

Genera las dos gemelas usando mi foto real como referencia. El identity lock va en el prompt.

Python + Pillowalineación

10 líneas que corrigen el encuadre desplazado. El paso que nadie te cuenta.

CSS clip-pathefecto

El spotlight que sigue al cursor. Corre en GPU, sin una sola librería.

Replícalo paso a paso

El código, las conexiones y los errores que ya cometí por ti.

  1. Genera la reveal con identity lock

    La imagen que se asoma bajo el cursor sale de ChatGPT usando tu foto real como referencia. La clave es el candado de identidad: le prohíbes tocar a la persona y solo le permites transformar el mundo alrededor. Este es el prompt real que usé:

    prompt · imagen reveal (ChatGPT)
    Use the attached image as the reference. Keep the woman EXACTLY as she is:
    do not change her face, skin, hair, expression, body, pose, outfit, the phone
    in her hands, or the framing and crop. Her identity must stay 100% identical
    to the reference, same person, untouched.
    
    Only transform the WORLD around her into "AI-native command mode": the
    background city dissolves into a luminous, elegant orchestration of growth
    dashboards, flowing data streams and AI workflow nodes (minimal, not cluttered).
    Brand palette only: warm orange (#fe6f01), magenta-pink and soft lavender,
    glowing over a cream base. A subtle glowing letter "W" emblem of light hovers
    beside her as her insignia. Soft orange and magenta rim light along the edge of
    her blazer and hair. Cinematic, premium, optimistic, semi-realistic editorial
    quality. Keep the same vertical 2:3 crop and the negative space on the left.
    No text, no logos, no watermark.
    
    Negative prompt: cartoon, flat illustration, anime, distorted face,
    extra fingers, deformed hands, cluttered, busy background, text, logos,
    watermark, low quality, blurry face
  2. Pídele la gemela base

    Para que el efecto funcione, la imagen visible y la oculta deben ser gemelas: misma persona, misma pose, mismo encuadre. El truco es pedirle las dos al mismo modelo, en la misma sesión y con el mismo candado, para que el grano y el render coincidan. Solo cambia el mundo:

    prompt · gemela base (ChatGPT)
    Use the attached image as the reference. Keep the woman EXACTLY as she is:
    do not change her face, skin, hair, expression, body, pose, outfit, the phone
    in her hands, or the framing and crop. Her identity must stay 100% identical
    to the reference, same person, untouched.
    
    Render the SAME photo with no effects: keep the world natural and calm,
    soft warm light over a cream base, premium semi-realistic editorial quality.
    No dashboards, no data streams, no glow, no emblem. Keep the same vertical
    2:3 crop and the negative space on the left. No text, no logos, no watermark.
  3. Alinea las gemelas con 10 líneas de Pillow

    Aunque el prompt diga “mismo encuadre”, ChatGPT nunca devuelve el crop exacto: la mía regresó con un 5% de desplazamiento y la cara brincaba al pasar el cursor. Lo arreglé con un mini script de Pillow: escalar y recortar la base hasta que las dos queden pixel-perfect.

    python · align.py
    # align.py · alinea la gemela base con la reveal (Pillow)
    # ChatGPT devolvió la base ~5% desplazada: se corrige con resize + crop.
    from PIL import Image
    
    REF = "reveal.png"   # la imagen que manda (el encuadre correcto)
    SRC = "base.png"     # la gemela desalineada
    SCALE = 1.05         # factor de corrección (mi caso: 5%)
    DX, DY = 0, -18      # ajuste fino en píxeles (a ojo, iterando)
    
    ref = Image.open(REF)
    src = Image.open(SRC).convert("RGB")
    
    # 1. Escalar la base por el factor de corrección
    w, h = ref.size
    big = src.resize((round(w * SCALE), round(h * SCALE)), Image.LANCZOS)
    
    # 2. Recortar el centro (más el ajuste fino) al tamaño exacto de la reveal
    left = (big.width - w) // 2 + DX
    top = (big.height - h) // 2 + DY
    big.crop((left, top, left + w, top + h)).save("base-aligned.png")
  4. Móntalo: clip-path + lerp del cursor

    Dos imágenes apiladas. La de arriba (modo AI) se recorta con un clip-path circular que sigue al cursor con suavizado (lerp) dentro de un requestAnimationFrame. El radio abre rápido y cierra suave, y todo se compone en GPU:

    html + css + js · el efecto completo
    <div class="stage">
      <img class="base" src="base-aligned.png" alt="" />
      <img class="reveal" src="reveal.png" alt="" />
    </div>
    
    <style>
      .stage { position: relative; }
      .stage img { position: absolute; inset: 0; width: 100%; }
      .reveal { clip-path: circle(var(--r, 0px) at var(--x) var(--y)); }
      @media (prefers-reduced-motion: reduce) { .reveal { display: none; } }
    </style>
    
    <script>
      const stage = document.querySelector(".stage");
      let rawX = 0, rawY = 0, x = 0, y = 0, r = 0, targetR = 0;
    
      stage.addEventListener("mousemove", (e) => {
        const b = stage.getBoundingClientRect();
        rawX = e.clientX - b.left;
        rawY = e.clientY - b.top;
        targetR = 150; /* radio del foco */
      });
      stage.addEventListener("mouseleave", () => { targetR = 0; });
    
      (function frame() {
        x += (rawX - x) * 0.14; /* lerp: el viaje suave del foco */
        y += (rawY - y) * 0.14;
        /* abre rápido (0.16), cierra suave (0.08) */
        r += (targetR - r) * (targetR > r ? 0.16 : 0.08);
        stage.style.setProperty("--x", x.toFixed(1) + "px");
        stage.style.setProperty("--y", y.toFixed(1) + "px");
        stage.style.setProperty("--r", Math.max(0, r).toFixed(1) + "px");
        requestAnimationFrame(frame);
      })();
    </script>
  5. El fallback también es diseño

    En pantallas táctiles el dedo hace de cursor, y con prefers-reduced-motion la capa reveal se desactiva y queda la foto base. Un efecto que marea no es un efecto premium.

Esto es una probadita de la Wen Escuelita

Estoy armando el lugar donde te enseño a conectar todo esto, en español y sin humo. Déjame tu correo y entras primera.

Sigue con estos