Webview 里不规则揭幕动画整层消失:SVG/CSS 滤镜 fail-closed,用 canvas
给一个桌面应用的启动页做进场动画:一张全屏墨色遮罩,从中心向外烧开一个毛边的洞,逐渐露出底下的页面。平滑圆形版本用 mask-image: radial-gradient(...) 一气呵成,但一旦给洞边加"有机不规则感",遮罩层直接消失——整个底页瞬间全露,看起来像动画没有播放。
这不是动画逻辑写错了,是踩进了浏览器渲染管线的一个 fail-closed 规范陷阱。
现象
三种常见的毛边写法,在 WKWebView / Chrome 里都会让遮罩整层消失:
- 在被遮罩元素上挂
filter: url(#burn-noise)(引用 SVGfeTurbulence+feDisplacementMap) - 父层挂
filter、子层挂mask,拆分规避——仍然消失 mask-image叠多个偏心径向渐变 +mask-composite: intersect(或-webkit-mask-composite: source-in)——覆盖阶段遮罩画不出来
症状极容易被误判为"动画没触发"或"状态机逻辑出错",实际是渲染层根本没画。
根因
CSS Filter 是 fail-closed 的
CSS Filter Effects 规范规定:当 filter: url(#id) 引用的 SVG 滤镜产出空结果或退化结果,整个元素不渲染。
feDisplacementMap 作用在一个半径为 0 的扩张圆上时,滤镜区域退化为空——动画从 r=0 起步必然触发这个条件。不是偶发,是从第一帧就挂了。
Chrome 实现里,同一元素同时挂 CSS mask 和 SVG filter 会发生渲染层互相清空的问题。即使你把它们拆到父子两层,合成仍然会踩中相同路径。mask-composite: intersect 在多层场景下同样会把整层算成空 mask,覆盖期连幕都画不出来。
这三条失效路径有一个共性:依赖运行时 SVG/CSS 滤镜对动态绘制区域的合成。Webview 对这类边界情况 fail-closed,不是 bug,是规范行为。
正解
放弃运行时滤镜,改用 canvas 2D 的 destination-out 合成模式手动绘制毛边洞。canvas 2D 是原始绘制调用,不经过 CSS 合成管线,不会 fail-closed。
// 每帧执行
ctx.fillStyle = inkGradient;
ctx.fillRect(0, 0, W, H); // 茶墨幕铺满
if (burning) {
const baseR = easeOut(t) * maxR;
ctx.save();
ctx.filter = 'blur(5px)'; // canvas 内部 blur,不影响渲染层
ctx.globalCompositeOperation = 'destination-out'; // 挖洞
// 不规则洞边:每个角度的半径 = baseR × (1 + AMP × 噪声偏移)
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const th = (i / N) * 2 * Math.PI;
const r = baseR * (1 + AMP * off[i % N]);
const x = cx + Math.cos(th) * r;
const y = cy + Math.sin(th) * r;
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.restore();
}几个关键细节:
- 毛边噪声:
off[i] = Σ aₖ·sin(fₖ·θ + φₖ),频率fₖ取整数谐波(3/5/8/13…)保证沿圆周无缝拼接;随机相位让每次启动形状不同。幅度AMP ≈ 0.16是毛边感和可读性之间的平衡点。 - maxR:
hypot(W, H) / 2 + margin,确保洞能盖住四角。 ctx.filter = 'blur(Npx)':canvas 内部的 blur 在 Chrome/WKWebView 支持良好,和 CSSfilter: url()fail-closed 是两回事,可以放心用作软化洞边。
首帧闪现(必修)
canvas overlay 挂载后要等第一个 requestAnimationFrame 才填色,中间会有一帧露出底页。修法:
- 用
useLayoutEffect而非useEffect建 canvas(浏览器绘制前同步执行) - 挂载时立刻同步调用一次
ctx.fillRect,不等 rAF
canvas 2D 合成操作走原始绘制路径,绕开 CSS Filter 的 fail-closed 规范;useLayoutEffect 配合挂载时同步填色,消除首帧底页闪现。两者合用,不规则揭幕动画在 WKWebView/Chrome 下稳定可靠。
排查提示
如果你遇到类似消失症状,先确认:平滑的单个 radial-gradient CSS mask 能否正常渲染?能渲染说明管线没问题,问题就在毛边技术选型上,直接换 canvas。
另外,这类 <2s 的一次性进场动画极难用无头截图工具观测(构建缓存 + 快 easing 让截图落在动画末段,看起来正常)。建议用截图工具先"预热"一次页面再截,或改用 canvas.toDataURL() 在特定帧主动存图验证。
一句话外卖
在 Webview 里做不规则 mask 动画,CSS
filter: url()和mask-composite都是 fail-closed 的——洞从 r=0 起步必然一帧也画不出来;换成 canvas 2Ddestination-out手绘,才是可靠出路。