@keyframes tv-flicker 通过非均匀时间点(12%、15%密集变化)、像素级transform抖动与0.85–0.98 opacity脉冲,精准模拟CRT闪屏的信号干扰感;需配合steps(1,end)、will-change及prefers-reduced-motion媒体查询以保障性能与可访问性。

用 @keyframes 模拟老电视闪屏的视觉节奏
老电视闪屏不是均匀闪烁,而是带“抖动+亮度突变+轻微扫描线残留”的复合效果。纯用 opacity 从 0 到 1 来回切会显得像灯泡开关,完全失真。
关键在于分层控制:主亮度脉冲(每 1.2–1.8 秒一次)、微抖动(高频小位移)、叠加噪声纹理(可选)。下面这个 @keyframes 是实测最接近 CRT 感觉的节奏:
@@keyframes tv-flicker {
0% { opacity: 0.97; transform: translate(0, 0); }
12% { opacity: 0.85; transform: translate(-0.3px, 0.2px); }
15% { opacity: 0.92; transform: translate(0.1px, -0.1px); }
28% { opacity: 0.98; transform: translate(0, 0); }
100% { opacity: 0.97; transform: translate(0, 0); }
}
- 时间点不是等距的——12% 和 15% 的密集变化模拟电子束回扫时的不稳定
-
transform用像素级偏移(px),避免用rem或百分比,否则在高 DPI 屏上会糊掉或不动 - 全程
opacity不低于 0.85,真实 CRT 不会全黑,只是“暗一帧”
把动画加到 body 上但别影响交互
直接写 body { animation: tv-flicker 1.5s infinite; } 看似简单,实际会拖慢滚动、卡住表单聚焦,甚至让 :hover 延迟响应。
必须加两个限制条件:
立即学习“前端免费学习笔记(深入)”;
- 用
animation-timing-function: steps(1, end)替代默认缓动,强制每一帧硬切,减少 GPU 插值开销 - 加
will-change: opacity, transform;提前告知浏览器哪些属性会变,避免重排重绘 - 动画只在空闲时触发:用
@media (prefers-reduced-motion: no-preference)包一层,尊重系统设置
最终生效的规则长这样:
@media (prefers-reduced-motion: no-preference) {
body {
animation: tv-flicker 1.6s infinite;
animation-timing-function: steps(1, end);
will-change: opacity, transform;
}
}
为什么不用 filter: brightness()?
很多人第一反应是用 brightness() 控制明暗,但它在 Safari 和旧版 Edge 中兼容性差,且和 transform 同时动画时容易触发强制重绘,帧率暴跌。
更实际的问题是语义错位:brightness() 改的是整个元素的光照模型,而老电视闪屏本质是信号干扰导致的局部亮度塌陷+位置偏移,opacity + transform 组合更贴近物理表现。
- Chrome 115+、Firefox 120+、Safari 16.4+ 都稳定支持
opacity+transform的合成动画 - 如果非要加扫描线感,用
background叠一层半透明条纹图(linear-gradient),别用filter: url(#scanlines),SVG 滤镜在移动端掉帧严重
移动端要关掉动画或降频
iPhone 和安卓中低端机跑 60fps 的 tv-flicker 动画非常吃力,尤其页面有轮播图或 canvas 时,会明显发热降频。
- 用
@media (max-width: 768px), (hover: none)直接禁用动画(触屏设备大概率不需要这种效果) - 或者把动画周期拉长到 2.8s 以上,降低视觉刺激强度,同时加
animation-play-state: paused,仅在用户停留超 3 秒后用 JS 触发play - 绝对不要监听
scroll或resize去动态启停动画——触发太频繁,JS 执行本身就会卡顿
老电视效果的“假”,恰恰藏在节奏不规律和轻微失控里;做得太准,反而不像了。










