效果概述
组件实现了一个"图片淡入 + 光带对角线扫过"的组合动效:
- 图片从透明渐显(淡入)
- 淡入完成后,一道白色光带从左上角扫向右下角
- 光带扫过后自然消失
整个动画约 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) ────────────────→|
| | | |
编排逻辑:
- 0-1000ms:图片从透明渐显,前 10% 时间保持透明(即前 100ms 不动),然后线性淡入
- 500ms:扫光动画开始(
animation-delay: 500ms),此时图片已经半透明可见 - 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编排多段动画的时序