效果概述

组件实现了一个"图片淡入 + 光带对角线扫过"的组合动效:

  1. 图片从透明渐显(淡入)
  2. 淡入完成后,一道白色光带从左上角扫向右下角
  3. 光带扫过后自然消失

整个动画约 2 秒,一次性播放,不循环。

核心实现

组件结构(约 40 行)

import React, { memo } from "react";
import classNames from "classnames";
import Style from "./index.module.less";

interface ISweepLightProps {
  src: string; // 图片 URL
  width: number; // 图片宽度
  className?: string; // 自定义样式类名
  onClick?: () => void;
}

function SweepLight(props: ISweepLightProps) {
  const { src = "", width, className = "", onClick = () => {} } = props;
  return (
    <div
      className={classNames(Style.sweepLight, className)}
      style={{
        WebkitMask: `url(${src}) no-repeat center / cover`,
        mask: `url(${src}) no-repeat center / cover`,
      }}
      onClick={onClick}
    >
      <img className={Style.img} width={width} src={src} alt="未找到图片" />
    </div>
  );
}

export default memo(SweepLight);

组件本身非常精简,核心逻辑全部交给 CSS。这是一个值得学习的设计思路——动效类组件应该让 CSS 做它擅长的事。

关键技术点 1:CSS Mask 实现不规则裁剪

style={{
  WebkitMask: `url(${src}) no-repeat center / cover`,
  mask: `url(${src}) no-repeat center / cover`,
}}

这是整个组件最精妙的设计。mask 属性用图片自身作为遮罩:

  • 图片的不透明区域 → 可见
  • 图片的透明区域 → 隐藏

这意味着无论图片是什么形状(圆形勋章、不规则徽章、带透明通道的 PNG),扫光效果都会严格沿着图片轮廓走,不会溢出到矩形边界外。

┌─────────────────────┐
│  ┌───────────────┐  │
│  │   ██████████  │  │  ← mask 区域(图片不透明部分)
│  │  ████████████ │  │     扫光只在这里可见
│  │   ██████████  │  │
│  └───────────────┘  │
│     透明区域         │  ← 扫光在这里被遮罩隐藏
└─────────────────────┘

WebkitMask 是为了兼容 iOS Safari 和旧版 Android WebView

关键技术点 2:伪元素 + 渐变实现光带

.sweepLight {
  position: relative;
  overflow: hidden;

  &::after {
    content: "";
    position: absolute;
    inset: -30%;
    background: linear-gradient(
      110deg,
      rgba(255, 255, 255, 0) 40%,
      rgba(255, 255, 255, 0.7),
      rgba(255, 255, 255, 0) 60%
    );
    transform: translate(-100%, -100%);
  }
}

几个细节值得注意:

inset: -30%:伪元素比容器大 60%。这是为了让光带在进入和离开时有足够的"跑道",避免在容器边缘突然出现或消失。

linear-gradient(110deg, ...):110 度角的渐变,形成一条从左上到右下的斜向光带。渐变结构是"透明 → 白色半透明 → 透明",中间亮两边暗,模拟真实光线扫过的效果。

40%60%:光带只占渐变宽度的 20%,这让光带看起来锐利而不是模糊一片。

关键技术点 3:动画编排

// 容器淡入
.sweepLight {
  opacity: 0;
  animation: sweep-fade-in 1s linear 1 forwards;

  &::after {
    animation: shark-wrap 1.5s linear 1 forwards;
    animation-delay: 500ms;
    transform: translate(-100%, -100%);
    opacity: 0;
  }
}

// 淡入动画
@keyframes sweep-fade-in {
  10% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

// 扫光动画
@keyframes shark-wrap {
  0% {
    transform: translate(-100%, -100%);
    opacity: 0;
  }
  80% {
    opacity: 1;
  }
  100% {
    transform: translate(100%, 100%);
    opacity: 0;
  }
}

动画时间线:

0ms        500ms       1000ms      1500ms      2000ms
|           |           |           |           |
|← 图片淡入 (1s) ──────→|           |           |
            |← 扫光动画 (1.5s) ────────────────→|
            |           |           |           |

编排逻辑:

  1. 0-1000ms:图片从透明渐显,前 10% 时间保持透明(即前 100ms 不动),然后线性淡入
  2. 500ms:扫光动画开始(animation-delay: 500ms),此时图片已经半透明可见
  3. 500-2000ms:光带从左上 (-100%, -100%) 移动到右下 (100%, 100%),同时自身透明度先升后降

animation-delay: 500ms 是关键——让扫光在图片"半显"时启动,用户先看到图片轮廓,再看到光带扫过,层次感更强。

关键技术点 4:forwards 保持终态

animation: sweep-fade-in 1s linear 1 forwards;

forwards 让动画结束后保持最后一帧的状态。没有它,图片淡入后会跳回 opacity: 0。这是 CSS 动画的常见陷阱。

兼容性处理

特性兼容方案
CSS Mask同时写 WebkitMask 和标准 mask
inset 属性现代浏览器支持良好,旧版可降级为 top/right/bottom/left
CSS 动画Android 4.1+ / iOS 8+ 均支持,无需 polyfill

小结

这个扫光组件用不到 40 行 TSX + 50 行 Less 实现了一个生产级的动效,核心思路是:

  • 用 CSS Mask 让光效适配任意形状的图片
  • 用伪元素 + 渐变生成光带,零额外 DOM
  • transform 驱动动画,确保 GPU 加速
  • animation-delay 编排多段动画的时序