我在 React Canvas 里做了一个液态玻璃透镜效果
最近我在 react-canvas 里做了一版「液态玻璃透镜」后处理,目标是:
- 透镜中心有放大感
- 透镜边缘有明显的玻璃扭曲和轻微色散
- 不引入第二套渲染栈,直接复用现有 CanvasKit 渲染管线
这篇文章记录一下这个效果的实现思路、关键代码和踩坑。
项目与线上地址
- GitHub 仓库:github.com/ouzhou/reac…
- 线上演示:react-canvas-design.vercel.app/#/juejin
为什么要做成后处理(Post Process)
一开始看起来最直觉的方案是:在页面上叠一个额外 canvas 或 WebGL 层专门做透镜。
但这会带来两个问题:
- 两套渲染结果同步成本高(滚动、缩放、相机状态都要对齐)
- 交互命中(pick)和视觉效果容易打架
最终采用的是:在同一条 CanvasKit 管线中做全屏后处理。
流程非常直接:
- 先把场景完整绘制到离屏 surface
- 对离屏图像应用 SkSL RuntimeEffect
- 再把结果一次性绘制到主画布
这样做的好处是:
- 场景逻辑完全复用,不需要维护双渲染树
- 效果控制集中在 shader 和 uniforms
- 与既有渲染架构兼容性高
透镜效果拆解
当前液态玻璃由 4 部分组成:
- 中心放大:透镜内部有明显放大
- 边缘扭曲:扭曲主要集中在外圈
- 色散(RGB 偏移):边缘轻微彩边,增强“玻璃感”
- 边缘高光 + 阴影:让透镜有实体感
其中我重点优化了一个点:
中心不需要扭曲,只要放大;扭曲集中在边缘约 20% 的环带。
具体做法是引入归一化半径 t,再用:
rim = smoothstep(0.8, 1.0, t)作为边缘权重- 中心(
t < 0.8)扭曲权重接近 0 - 靠近边缘(
t -> 1)扭曲和色散逐渐增强
这样视觉上更像“镜片边缘折射”,而不是整块区域都在抖动。
动画与性能:从“永远重绘”到“按需重绘”
透镜是纯 uniform 驱动的后处理,如果每帧都强制重绘,虽然简单,但成本偏高。
我把策略改成了:
- 指针移动时通过
requestCanvasRepaint(canvas)主动唤醒 shouldContinueRepaint只在透镜还在追目标点时继续下一帧- 透镜静止后自动停止连续重绘
这个改动对体感影响很明显:移动时顺滑,停下时不再空转。
坐标与边界处理
Web 端最容易出错的是坐标系和边界采样:
- 指针使用
getBoundingClientRect()+ backing store 比例换算到像素坐标 - shader 采样 UV 做
clamp(0..1),避免边缘出现黑边或抽样越界伪影
这些看似小问题,实际会直接影响“高级感”。
我这次的收获
- 视觉特效里,“哪里不做效果”往往和“做什么效果”一样重要
- 透镜这种效果,中心稳定 + 边缘强表达,比全域扭曲更自然
- 架构层做对(统一后处理管线)之后,后续微调成本会非常低
如果你也在做 Canvas/Skia 场景里的玻璃、景深、调色类效果,我非常建议优先走「场景离屏 + RuntimeEffect」这条路,扩展性会好很多。