<div id="root">
</div>
@import url('//fonts.googleapis.com/css?family=Roboto');

.mosaicEffect {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;

  display: grid;
  grid-template-rows: repeat(10, 1fr);
  grid-template-columns: repeat(20, 1fr);
}

// animation
@keyframes toClear {
  0% {
    background-color: #ffff;
    backdrop-filter: blur(50px);
  }

  50% {
    background-color: #fff0;
    backdrop-filter: blur(50px);
  }

  100% {
    background-color: #fff0;
    backdrop-filter: blur(0);
  }
}

.cell {
  background-color: #fff;
  [data-intersecting] & {
    will-change: background-color backdrop-filter;
  }
  [data-intersecting='true'] & {
    &[data-param="0"] {
      background-color: transparent;
      animation: toClear 150ms steps(3) forwards;
    }

    &[data-param="1"] {
      animation: toClear 150ms steps(3) 50ms forwards;
    }

    &[data-param="2"] {
      animation: toClear 150ms steps(3) 100ms forwards;
    }

    &[data-param="3"] {
      animation: toClear 150ms steps(3) 150ms  forwards;
    }
  }
}

// ----------

html, * {
  font-family: 'Roboto';
  box-sizing: border-box;
}

body {
  padding: 1.5em 0 4em;
}

.title {
  text-align: center;
  margin: 0 auto 1em;
  font-size: 1.5em;
  font-weight: 700;
}

.figure {
  diplay: block;
  position: relative;
  aspect-ratio: 2;
  margin: auto;
  width: min(80vw, 600px);
  background-color: whitesmoke;
  
  ~ .figure {
    margin-top: 4em;
  }
}

.caption {
  position: absolute;
  top: 100%;
  margin: 0.5em 0;
}

.button {
  position: absolute;
  top: 100%;
  right: 0;
  white-space: nowrap;
  margin: 0.5em 0;
  
  ~ .button {
    right: 8em;
  }
}

.image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  vertical-align: top;
}
View Compiled
import { FC, useEffect, useState, useCallback } from "https://cdn.skypack.dev/react@17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";


const cellParams = [
    1,
    0,
    1,
    2,
    2,
    2,
    3,
    2,
    1,
    1,
    3,
    1,
    0,
    1,
    3,
    1,
    2,
    0,
    2,
    1,
    2,
    1,
    0,
    2,
    0,
    3,
    1,
    1,
    1,
    3,
    2,
    1,
    1,
    0,
    3,
    0,
    1,
    3,
    3,
    0,
    3,
    0,
    0,
    2,
    3,
    1,
    3,
    3,
    1,
    1,
    1,
    3,
    3,
    2,
    0,
    2,
    0,
    0,
    1,
    0,
    0,
    3,
    0,
    1,
    1,
    2,
    3,
    1,
    3,
    0,
    0,
    3,
    3,
    3,
    3,
    3,
    0,
    2,
    2,
    2,
    3,
    3,
    2,
    3,
    1,
    3,
    3,
    2,
    0,
    0,
    1,
    1,
    2,
    2,
    3,
    2,
    1,
    0,
    0,
    3,
    0,
    0,
    3,
    0,
    2,
    3,
    0,
    3,
    1,
    1,
    0,
    1,
    2,
    0,
    0,
    2,
    3,
    2,
    2,
    2,
    3,
    2,
    0,
    1,
    1,
    1,
    2,
    3,
    1,
    3,
    0,
    3,
    3,
    2,
    2,
    3,
    2,
    1,
    1,
    2,
    1,
    2,
    3,
    3,
    1,
    0,
    2,
    1,
    0,
    1,
    3,
    0,
    0,
    2,
    1,
    2,
    1,
    3,
    3,
    0,
    1,
    0,
    0,
    2,
    2,
    3,
    3,
    2,
    1,
    0,
    2,
    2,
    1,
    0,
    0,
    0,
    1,
    3,
    0,
    2,
    0,
    1,
    3,
    0,
    0,
    3,
    2,
    1,
    2,
    2,
    2,
    2,
    2,
    0,
    1,
    2,
    1,
    0,
    3,
    0
];

const image_src = "https://images.unsplash.com/photo-1688550378756-866114814fee?crop=entropy&cs=srgb&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2ODg3MTc0OTN8&ixlib=rb-4.0.3&q=85";


export const MizaicEffect: FC<PixelateEffectProps> = ({ addClass = [] }) => {
  
  const [isIntersecting, setIsIntersecting] = useState<boolean>(false);
  
  const handleAdd = useCallback(()=> {
    setIsIntersecting(true);
  },[isIntersecting]);
  
  const handleRemove = useCallback(()=> {
    setIsIntersecting(false);
  },[isIntersecting]);
  
  return(
    <div
      data-intersecting={isIntersecting}
      className={["mosaicEffect", ...addClass].join(" ")}
    >
      {cellParams.map((grid, index) => (
        <div key={index} className={"cell"} data-param={grid}></div>
      ))}
      <button className={"button"} disabled={isIntersecting} onClick={handleAdd}>Mosaicize</button>
      <button className={"button"} disabled={!isIntersecting} onClick={handleRemove}>Reset</button>
    </div>
  );
};

ReactDOM.render(
  <React.StrictMode>
    <h1 className="title">Mosaic Effect</h1>
    <figure class="figure">
      <MizaicEffect />
      <img class="image" src={image_src} alt="" />
      <figcaption className="caption">Unprocessed image + Mosaic effect
</figcaption>
    </figure>
    <figure class="figure">
      <img class="image" src={image_src} alt="" />
      <figcaption className="caption">Unprocessed image</figcaption>
    </figure>
  </React.StrictMode>,
  document.getElementById('root')
);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js