组件
颗粒纹理遮罩 Grain Overlay
利用 SVG 分形噪点(feTurbulence)滤镜为卡片、大图与渐变背景增添电影级胶片质感与材质温度的纹理组件。
基础用法
在摄影封面上叠加胶片颗粒,并开启 animated 让颗粒像放映中的胶片一样跳动:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/grain-overlay安装基础依赖
pnpm add lucide-react class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/grain-overlay.tsx"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface GrainOverlayProps extends Omit<
React.ComponentProps<"svg">,
"opacity"
> {
/** Grain opacity. @default 0.16 */
opacity?: number
/** Base turbulence frequency. @default 0.72 */
frequency?: number
/** Number of fractal noise octaves. @default 3 */
octaves?: number
/** Deterministic noise seed. @default 8 */
seed?: number
/** CSS blend mode used by the overlay. @default "soft-light" */
blendMode?: React.CSSProperties["mixBlendMode"]
/** Jitter the grain like projected film. The parent must clip overflow. @default false */
animated?: boolean
/** Grain frames per second when `animated` is enabled. @default 10 */
fps?: number
}
/** Adds a scalable SVG fractal-noise texture over a positioned surface. */
function GrainOverlay({
opacity = 0.16,
frequency = 0.72,
octaves = 3,
seed = 8,
blendMode = "soft-light",
animated = false,
fps = 10,
className,
style,
...props
}: GrainOverlayProps) {
const svgRef = React.useRef<SVGSVGElement>(null)
const filterId = `grain-${React.useId().replaceAll(":", "")}`
React.useEffect(() => {
const svg = svgRef.current
if (!animated || !svg) return
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
// The oversized layer is shifted between random offsets; the noise itself is rasterized once.
const timer = window.setInterval(() => {
const x = (Math.random() * 2 - 1) * 10
const y = (Math.random() * 2 - 1) * 10
svg.style.transform = `translate3d(${x}%, ${y}%, 0)`
}, 1000 / fps)
return () => window.clearInterval(timer)
}, [animated, fps])
return (
<svg
ref={svgRef}
aria-hidden="true"
data-slot="grain-overlay"
className={cn(
"pointer-events-none absolute inset-0 size-full select-none",
className
)}
style={{
...(animated && {
inset: "-50%",
width: "200%",
height: "200%",
willChange: "transform",
}),
...style,
opacity,
mixBlendMode: blendMode,
}}
{...props}
>
<filter id={filterId} x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence
type="fractalNoise"
baseFrequency={frequency}
numOctaves={octaves}
seed={seed}
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="100%" height="100%" filter={`url(#${filterId})`} />
</svg>
)
}
export { GrainOverlay }
属性 Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| opacity | number | 0.16 | 噪点纹理的整体不透明度(0 到 1 之间)。 |
| frequency | number | 0.72 | SVG feTurbulence 的基底噪声频率。数值越高,颗粒越细腻紧密。 |
| octaves | number | 3 | 分形噪声的倍频层数,层数越多细节越丰富。 |
| seed | number | 8 | 生成随机噪点的固定种子数,保证渲染结果一致确定。 |
| blendMode | React.CSSProperties['mixBlendMode'] | "soft-light" | CSS 混合模式(如 'soft-light'、'overlay'、'multiply'、'screen' 等)。 |
| animated | boolean | false | 是否让颗粒逐帧跳动。实现方式是平移一张放大的噪点层,噪点本身只光栅化一次;父元素需要 `overflow-hidden`。 |
| fps | number | 10 | `animated` 开启时颗粒的跳动帧率。8–12 帧最接近胶片质感。 |
| className | string | — | 应用于 SVG 纹理图层的 CSS 类名(默认包含 absolute inset-0 size-full pointer-events-none)。 |
事件 Events
该组件为纯装饰 SVG 滤镜遮罩,不包含业务交互事件。
使用场景与设计规范
GrainOverlay 适用于暗色模式渐变消除色阶断层、复古设计风、高阶品牌宣传卡片:
- 解决 CSS 渐变色带问题(Color Banding):在暗色渐变(如从
#09090b到#18181b)中,人眼很容易察觉到阶梯状断层,叠加一层opacity={0.18}的噪点即可让过渡变得无比顺滑自然。 - 纯矢量计算:基于 SVG 原生滤镜,文件体积近乎为零,不需要额外加载几十 KB 的 PNG 噪点贴图。
场景示例
强度对比
同一纯色表面上,不同不透明度的 soft-light 颗粒带来的质感差异:
Loading…
无障碍与交互 Accessibility
- 绝对隔离:默认具备
pointer-events-none与user-select: none,绝不干扰用户选中卡片文字或点击按钮。 - 无障碍树忽略:内置
aria-hidden="true",屏幕阅读器完全忽略该装饰图层。 - 动效减弱适配:系统开启“减少动态效果”时,
animated不会启动,颗粒保持静止。