流体黏腻 SVG 擦除动画|涂抹揭隐内容(GSAP)

HTML

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Ashleybrookecs Smudge Revealer | Codegrid</title>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <section class="hero">
      <div class="hero-content-foreground">
        <h1>Dig in</h1>
      </div>

      <div class="hero-content-background">
        <h3>
          The things worth finding are never on the surface. They live in the
          parts you almost scrolled past.
        </h3>
      </div>

      <svg
        xmlns="http://www.w3.org/2000/svg"
        preserveAspectRatio="none"
        class="smudge-revealer"
      >
        <defs>
          <filter id="smudge-goo">
            <feGaussianBlur in="SourceGraphic" stdDeviation="25" />
            <feColorMatrix
              type="matrix"
              values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 60 -14"
            />
          </filter>
        </defs>
        <mask id="smudge-mask">
          <g class="smudge-blobs" filter="url(#smudge-goo)"></g>
        </mask>
      </svg>
    </section>

    <script type="module" src="/script.js"></script>
  </body>
</html>

js

import gsap from "gsap";

const config = {
  smoothing: 0.1,
  movementThreshold: 0.01,
  sizeFromSpeed: 0.2,
  expandMultiplier: 2,
  expandTime: 2,
  expandEase: "power1.inOut",
  dissolveStart: 2,
  dissolveTime: 3,
  dissolveEase: "power3.in",
};

const heroSection = document.querySelector(".hero");
const smudgeSVG = document.querySelector(".smudge-revealer");
const smudgeContainer = document.querySelector(".smudge-blobs");

const pointer = { x: 0, y: 0 };
const smoothPointer = { x: 0, y: 0 };
let hasStarted = false;

function onPointerMove(x, y) {
  if (!hasStarted) {
    pointer.x = smoothPointer.x = x;
    pointer.y = smoothPointer.y = y;
    hasStarted = true;
    return;
  }

  pointer.x = x;
  pointer.y = y;
}

heroSection.addEventListener("mousemove", function (e) {
  onPointerMove(e.pageX, e.pageY);
});

heroSection.addEventListener(
  "touchstart",
  function (e) {
    e.preventDefault();
    onPointerMove(e.touches[0].pageX, e.touches[0].pageY);
  },
  { passive: false },
);

heroSection.addEventListener(
  "touchmove",
  function (e) {
    e.preventDefault();
    onPointerMove(e.touches[0].pageX, e.touches[0].pageY);
  },
  { passive: false },
);

function matchSVGToViewport() {
  smudgeSVG.style.width = window.innerWidth + "px";
  smudgeSVG.style.height = window.innerHeight + "px";
}

matchSVGToViewport();
window.addEventListener("resize", matchSVGToViewport);

function stampSmudgeAt(x, y, radius) {
  const circle = document.createElementNS(
    "http://www.w3.org/2000/svg",
    "circle",
  );

  circle.setAttribute("cx", x);
  circle.setAttribute("cy", y);
  circle.setAttribute("r", radius);
  circle.setAttribute("fill", "#fff");

  smudgeContainer.prepend(circle);

  const animatedRadius = { current: radius };

  const timeline = gsap.timeline({
    onUpdate() {
      circle.setAttribute("r", Math.max(0, animatedRadius.current));
    },
    onComplete() {
      timeline.kill();
      circle.remove();
    },
  });

  timeline.to(animatedRadius, {
    current: radius * config.expandMultiplier,
    duration: config.expandTime,
    ease: config.expandEase,
  });

  timeline.to(
    animatedRadius,
    {
      current: 0,
      duration: config.dissolveTime,
      ease: config.dissolveEase,
    },
    config.dissolveStart,
  );
}

function update() {
  if (hasStarted) {
    smoothPointer.x += (pointer.x - smoothPointer.x) * config.smoothing;
    smoothPointer.y += (pointer.y - smoothPointer.y) * config.smoothing;

    const speed = Math.hypot(
      pointer.x - smoothPointer.x,
      pointer.y - smoothPointer.y,
    );

    if (speed > config.movementThreshold) {
      stampSmudgeAt(
        smoothPointer.x,
        smoothPointer.y,
        speed * config.sizeFromSpeed,
      );
    }
  }

  requestAnimationFrame(update);
}

requestAnimationFrame(update);

css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

h1,
h3 {
  text-transform: uppercase;
  font-family: "Hanson", sans-serif;
  line-height: 0.9;
}

h1 {
  font-size: clamp(5rem, 22.5vw, 30rem);
}

h3 {
  font-size: clamp(3rem, 5vw, 6rem);
}

.hero {
  position: relative;
  width: 100%;
  height: 100svh;
  overflow: hidden;
}

.hero-content-background,
.hero-content-foreground {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 2rem;
  text-align: center;
  user-select: none;
}

.hero-content-foreground {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  background-color: #2a2b2a;
  color: #edf2ed;
}

.hero-content-background {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #cbd4c2;
  color: #323332;
  mask: url(#smudge-mask);
  -webkit-mask: url(#smudge-mask);
}

.smudge-revealer {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
让链接同时具备两种打开方式
获取源码: CODE
下载数:1人次, 文件大小: 4.7 KB, 上传日期: 2026年-5 月-11日

公众号回复:gcode 获取解压密码

qrcode_for_gh_6ea2c28a1709_258 (1)

29 人查阅
Avatar photo

code