KailexKailex

( COMPONENTES )

Revelado de imagen pixelado

Portada de Revelado de imagen pixelado
Vista previa del componente

Documentación

Carga primero las librerías externas. Después copia la estructura y añade los bloques de código indicados a continuación.

Preparación: scripts externos

Pega este script en Before </body>, antes del JavaScript del componente. Si tu página ya carga GSAP, conserva una sola carga.

GSAP
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>

1. Copia la estructura a Webflow

Selecciona el contenedor de destino en el Designer y pega con Ctrl+V o Cmd+V. Las capas, las clases y los estilos base se entregan en el formato nativo de Webflow.

2. Añade el CSS custom

Pégalo en Page settings → Custom code → Inside <head>. Este bloque conserva los estados, selectores especiales y reglas responsive; las animaciones también se incluyen aquí cuando existen.

CSS
<style>
.cloneable { --container-padding: 2em; --color-neutral-800: #131313; --color-primary: #ff4c24;
}
.pixelated-image-card { overflow: hidden;
}
.pixelated-image-card__img { object-fit: cover; }
.pixelated-image-card__pixels { pointer-events: none; }
@media (max-width: 1024px) {
  .pixelated-image-card { width: 100%; max-width: 30em; }
}
</style>

3. Añade el JavaScript

Pégalo en Page settings → Custom code → Before </body>. Se inicializa al cargar la página. Mantén las clases y los atributos originales; puedes duplicar el componente sin duplicar el script.

JS
<script>
function initPixelatedImageReveal() {
  const animationStepDuration = 0.3;
  const gridSize = 7;
  const pixelSize = 100 / gridSize;
  const cards = document.querySelectorAll('[data-pixelated-image-reveal]');
  const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)');
  const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  cards.forEach((card) => {
    const pixelGrid = card.querySelector('[data-pixelated-image-reveal-grid]');
    const activeCard = card.querySelector('[data-pixelated-image-reveal-active]');
    if (!pixelGrid || !activeCard || card.dataset.pixelatedReady) return;
    card.dataset.pixelatedReady = 'true';
    pixelGrid.querySelectorAll('.pixelated-image-card__pixel').forEach(pixel => pixel.remove());

    for (let row = 0; row < gridSize; row++) {
      for (let col = 0; col < gridSize; col++) {
        const pixel = document.createElement('div');
        pixel.classList.add('pixelated-image-card__pixel');
        pixel.style.width = `${pixelSize}%`;
        pixel.style.height = `${pixelSize}%`;
        pixel.style.left = `${col * pixelSize}%`;
        pixel.style.top = `${row * pixelSize}%`;
        pixelGrid.appendChild(pixel);
      }
    }

    const pixels = pixelGrid.querySelectorAll('.pixelated-image-card__pixel');
    const staggerDuration = animationStepDuration / pixels.length;
    let isActive = false;
    let delayedCall;
    const controller = new AbortController();
    const options = { signal: controller.signal };
    const animatePixels = (activate) => {
      isActive = activate;
      gsap.killTweensOf(pixels);
      if (delayedCall) delayedCall.kill();
      gsap.set(pixels, { display: 'none' });
      if (reducedMotion.matches) {
        activeCard.style.display = activate ? 'block' : 'none';
        return;
      }
      gsap.to(pixels, {
        display: 'block', duration: 0,
        stagger: { each: staggerDuration, from: 'random' }
      });
      delayedCall = gsap.delayedCall(animationStepDuration, () => {
        activeCard.style.display = activate ? 'block' : 'none';
        activeCard.style.pointerEvents = activate ? 'none' : '';
      });
      gsap.to(pixels, {
        display: 'none', duration: 0, delay: animationStepDuration,
        stagger: { each: staggerDuration, from: 'random' }
      });
    };
    card.addEventListener('click', () => {
      if (!finePointer.matches) animatePixels(!isActive);
    }, options);
    card.addEventListener('mouseenter', () => {
      if (finePointer.matches && !isActive) animatePixels(true);
    }, options);
    card.addEventListener('mouseleave', () => {
      if (finePointer.matches && isActive) animatePixels(false);
    }, options);
    reducedMotion.addEventListener('change', () => animatePixels(isActive), options);
    window.addEventListener('pagehide', () => {
      controller.abort();
      gsap.killTweensOf(pixels);
      if (delayedCall) delayedCall.kill();
      delete card.dataset.pixelatedReady;
    }, { once: true });
  });
}
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initPixelatedImageReveal, { once: true });
} else initPixelatedImageReveal();
</script>

4. Personaliza y comprueba

Edita los textos, colores y espaciados base desde el Designer. Revisa el comportamiento en la vista previa con custom code habilitado o en el sitio publicado. Prueba también el teclado y la vista móvil.

Para integrarlo en una web con código, utiliza los bloques por separado y conserva las clases y los atributos de la estructura.

Preparación: scripts externos

Carga GSAP antes del JavaScript del componente. Incluye la librería una sola vez en tu página.

GSAP
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>

Añade la estructura en tu página.

HTML
<section class="cloneable">
  <div data-hover="" data-pixelated-image-reveal="" class="pixelated-image-card">
    <div class="before__100"></div>
    <div class="pixelated-image-card__default">
      <img src="https://cdn.prod.website-files.com/6712ad33825977f9d2f1ba2c/6714d43a777a77da89a9b5ec_osmo-pixelated-image-1.jpg" width="400" alt="Retrato antes del revelado" class="pixelated-image-card__img">
    </div>
    <div data-pixelated-image-reveal-active="" class="pixelated-image-card__active">
      <img src="https://cdn.prod.website-files.com/6712ad33825977f9d2f1ba2c/6714d43a4d1abab1b3c81caf_osmo-pixelated-image-2.jpg" width="400" alt="Retrato después del revelado" class="pixelated-image-card__img">
    </div>
    <div data-pixelated-image-reveal-grid="" class="pixelated-image-card__pixels" aria-hidden="true">
      <div class="pixelated-image-card__pixel"></div>
    </div>
  </div>
</section>

Añade estos estilos a tu hoja de estilos.

CSS
.cloneable {
  --container-padding: 2em;
  --color-neutral-800: #131313;
  --color-primary: #ff4c24;
  padding: var(--container-padding);
  justify-content: center;
  align-items: center;
  min-height: 100svh;
  display: flex;
  position: relative;
}
.pixelated-image-card {
  background-color: var(--color-neutral-800);
  color: var(--color-primary);
  border-radius: .5em;
  width: 30vw;
  max-width: 100%;
  position: relative;
  overflow: hidden;
}
.before__100 { padding-top: 100%; }
.pixelated-image-card__default,
.pixelated-image-card__img,
.pixelated-image-card__active,
.pixelated-image-card__pixels {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}
.pixelated-image-card__img { object-fit: cover; }
.pixelated-image-card__active { display: none; }
.pixelated-image-card__pixels { pointer-events: none; }
.pixelated-image-card__pixel {
  background-color: currentColor;
  width: 100%;
  height: 100%;
  display: none;
  position: absolute;
}
@media (max-width: 1024px) {
  .pixelated-image-card { width: 100%; max-width: 30em; }
}

Añade el script después de la estructura HTML.

JavaScript
function initPixelatedImageReveal() {
  const animationStepDuration = 0.3;
  const gridSize = 7;
  const pixelSize = 100 / gridSize;
  const cards = document.querySelectorAll('[data-pixelated-image-reveal]');
  const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)');
  const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  cards.forEach((card) => {
    const pixelGrid = card.querySelector('[data-pixelated-image-reveal-grid]');
    const activeCard = card.querySelector('[data-pixelated-image-reveal-active]');
    if (!pixelGrid || !activeCard || card.dataset.pixelatedReady) return;
    card.dataset.pixelatedReady = 'true';
    pixelGrid.querySelectorAll('.pixelated-image-card__pixel').forEach(pixel => pixel.remove());

    for (let row = 0; row < gridSize; row++) {
      for (let col = 0; col < gridSize; col++) {
        const pixel = document.createElement('div');
        pixel.classList.add('pixelated-image-card__pixel');
        pixel.style.width = `${pixelSize}%`;
        pixel.style.height = `${pixelSize}%`;
        pixel.style.left = `${col * pixelSize}%`;
        pixel.style.top = `${row * pixelSize}%`;
        pixelGrid.appendChild(pixel);
      }
    }

    const pixels = pixelGrid.querySelectorAll('.pixelated-image-card__pixel');
    const staggerDuration = animationStepDuration / pixels.length;
    let isActive = false;
    let delayedCall;
    const controller = new AbortController();
    const options = { signal: controller.signal };
    const animatePixels = (activate) => {
      isActive = activate;
      gsap.killTweensOf(pixels);
      if (delayedCall) delayedCall.kill();
      gsap.set(pixels, { display: 'none' });
      if (reducedMotion.matches) {
        activeCard.style.display = activate ? 'block' : 'none';
        return;
      }
      gsap.to(pixels, {
        display: 'block', duration: 0,
        stagger: { each: staggerDuration, from: 'random' }
      });
      delayedCall = gsap.delayedCall(animationStepDuration, () => {
        activeCard.style.display = activate ? 'block' : 'none';
        activeCard.style.pointerEvents = activate ? 'none' : '';
      });
      gsap.to(pixels, {
        display: 'none', duration: 0, delay: animationStepDuration,
        stagger: { each: staggerDuration, from: 'random' }
      });
    };
    card.addEventListener('click', () => {
      if (!finePointer.matches) animatePixels(!isActive);
    }, options);
    card.addEventListener('mouseenter', () => {
      if (finePointer.matches && !isActive) animatePixels(true);
    }, options);
    card.addEventListener('mouseleave', () => {
      if (finePointer.matches && isActive) animatePixels(false);
    }, options);
    reducedMotion.addEventListener('change', () => animatePixels(isActive), options);
    window.addEventListener('pagehide', () => {
      controller.abort();
      gsap.killTweensOf(pixels);
      if (delayedCall) delayedCall.kill();
      delete card.dataset.pixelatedReady;
    }, { once: true });
  });
}
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initPixelatedImageReveal, { once: true });
} else initPixelatedImageReveal();

Cómo funciona

Una cuadrícula de píxeles cubre la imagen y revela el segundo retrato. Al retirar el cursor vuelve a la imagen inicial; en pantallas táctiles, cada toque alterna los retratos.

Personalización

Ajusta animationStepDuration = 0.3 para acelerar o ralentizar cada fase del revelado. Cambia gridSize = 7 para modificar la cuadrícula: un valor mayor genera más píxeles, de menor tamaño. Sustituye las dos imágenes y ajusta los colores y el tamaño de la tarjeta.

Requisitos

Carga GSAP 3.15 antes del JavaScript del componente. En Webflow, copia la estructura nativa y añade los bloques de CSS y JavaScript indicados en Custom code. Conserva las clases y los atributos data-pixelated-image-reveal; comprueba el efecto en la vista previa con custom code habilitado o en el sitio publicado.