解密苹果最新 Liquid Glass 效果:如何用代码重现 iOS 设计系统的视觉魔法

🚀 探索100+强大的React Hooks可能性!访问 www.reactuse.com 获取完整文档和MCP支持,或通过 npm install @reactuses/core 安装,让我们丰富的Hook集合为您的React开发效率注入强劲动力!

楔子

苹果在最新的设计系统中引入了令人惊艳的 Liquid Glass 效果,这种视觉魔法让界面元素仿佛具有了真实玻璃的质感。最近在研究这个效果,发现核心原理其实就是让背景文字产生精确的偏移!听起来简单,用 backdropFilter 滤镜就行了,但问题是:怎么让滤镜的偏移效果看起来像真正的玻璃扭曲呢?

基本思路:给像素发搬家指令

可以把屏幕上的每个像素想象成一个小人,玻璃效果就是给这些小人下达"搬家指令"。下图清晰地展示了这个过程:

左边是原始的规整网格,右边展示了被液体玻璃扭曲后的效果。可以看到:

  • 中心区域被放大聚焦
  • 周围区域产生弯曲变形
  • 每个格子都还在,只是位置和形状发生了变化

问题是:怎样精确地告诉每个像素该往哪里搬?

第一个坑:CSS原生滤镜不够用

最开始我想直接用CSS的 backdropFilter

css 复制代码
.glass {
  backdrop-filter: blur(10px);  /* 只能模糊,不能精确控制像素移动 */
}

结果发现CSS原生滤镜虽然挺强大,但没法精确控制每个像素的移动方向和距离。它就只能搞搞模糊、亮度、对比度这些基础效果。

CSS搞不定,咋办?得想办法引入SVG滤镜来扩展CSS的能力。

解决方案:SVG滤镜接管像素移动

SVG有个厉害的滤镜叫 feDisplacementMap,这东西就像个"交通指挥员",能根据一张特殊的"指挥图"来让每个像素该往哪移往哪移。

把这个SVG滤镜嵌入到CSS的 backdropFilter 里:

typescript 复制代码
backdropFilter: `url(#${id}_filter) blur(0.25px) contrast(1.2) brightness(1.05) saturate(1.1)`

这里的 url(#${id}_filter) 就是秘密武器!但问题来了,这滤镜怎么知道每个像素该往哪移呢?

下一个问题:怎么做"移动指令地图"?

feDisplacementMap 得有张特殊的图片当"指挥地图",这图片用颜色来编码移动指令:

jsx 复制代码
<svg>
  <defs>
    <filter id={`${id}_filter`}>
      {/* 这张图片告诉每个像素该怎么移动 */}
      <feImage ref={feImageRef} />
      
      {/* 根据图片指令执行像素移动 */}
      <feDisplacementMap
        in="SourceGraphic"              // 输入:背景内容
        in2={`${id}_map`}               // 参考:移动指令地图
        xChannelSelector="R"            // 红色控制水平移动
        yChannelSelector="G"            // 绿色控制垂直移动
      />
    </filter>
  </defs>
</svg>

颜色编码的规则很简单:

  • 红色通道(R):控制水平移动(255=右移,128=不动,0=左移)
  • 绿色通道(G):控制垂直移动(255=下移,128=不动,0=上移)

现在问题变成了:怎么生成这张"移动指令地图"?

关键问题:玻璃的扭曲规律是啥?

要生成指令地图,得先搞清楚:真实的玻璃球是怎么扭曲背景的?

观察真实玻璃,能发现个规律:

  • 中心区域:像素几乎不动,看起来正常
  • 边缘区域:像素被"拉"向玻璃球中心
  • 过渡区域:从正常到扭曲,平滑过渡

要用代码描述这个规律,得:

  1. 先知道每个点是在玻璃内部、边缘还是外部
  2. 根据位置算出扭曲强度
  3. 决定像素的新位置

这就要用到数学工具:有向距离场(SDF)

数学建模:用距离场描述玻璃边界

想象你站在圆形游泳池旁边:

  • 池子里面:距离是负数(-2米)
  • 池子外面:距离是正数(+3米)
  • 池子边缘:距离是0

用代码表达这个概念:

typescript 复制代码
function roundedRectSDF(x: number, y: number, width: number, height: number, radius: number): number {
  const qx = Math.abs(x) - width + radius;
  const qy = Math.abs(y) - height + radius;
  return Math.min(Math.max(qx, qy), 0) + length(Math.max(qx, 0), Math.max(qy, 0)) - radius;
}

有了距离场,我们就能写出默认的玻璃扭曲逻辑:

typescript 复制代码
const defaultFragment = (uv) => {
  const ix = uv.x - 0.5;  // 转换到中心坐标系
  const iy = uv.y - 0.5;
  
  // 1. 计算到玻璃边缘的距离
  const distanceToEdge = roundedRectSDF(ix, iy, 0.3, 0.2, 0.6);
  
  // 2. 距离越近,扭曲强度越大
  const displacement = smoothStep(0.8, 0, distanceToEdge - 0.15);
  const scaled = smoothStep(0, 1, displacement);
  
  // 3. 所有像素都被"拉"向中心
  return texture(ix * scaled + 0.5, iy * scaled + 0.5);
};

现在我们有了数学模型,但还有最后一个问题:如何把这个数学公式变成SVG滤镜能理解的"指令地图"?

最后一步:生成并应用位移贴图

要解决这个问题,我们需要一个桥梁函数,把前面定义的数学模型转换成SVG滤镜能够理解的实际图片数据。这个桥梁就是 updateShader 函数:

typescript 复制代码
const updateShader = () => {
  // 1. 准备画布数据
  const data = new Uint8ClampedArray(w * h * 4);
  let maxScale = 0;
  const rawValues: number[] = [];
  
  // 2. 第一遍:用数学模型计算每个像素的新位置
  for (let i = 0; i < data.length; i += 4) {
    const x = (i / 4) % w;
    const y = Math.floor(i / 4 / w);
    
    // 这里调用我们刚才定义的扭曲逻辑
    const pos = fragmentShader({ x: x / w, y: y / h });
    
    // 计算这个像素需要移动多少
    const dx = pos.x * w - x;  // 水平移动距离
    const dy = pos.y * h - y;  // 垂直移动距离
    
    maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy));
    rawValues.push(dx, dy);
  }
  
  // 3. 第二遍:把移动距离编码成颜色
  let index = 0;
  for (let i = 0; i < data.length; i += 4) {
    const r = rawValues[index++] / maxScale + 0.5;  // 水平移动 → 红色
    const g = rawValues[index++] / maxScale + 0.5;  // 垂直移动 → 绿色
    
    data[i]     = r * 255;  // 红色通道
    data[i + 1] = g * 255;  // 绿色通道
    data[i + 2] = 0;        // 蓝色不用
    data[i + 3] = 255;      // 不透明
  }
  
  // 4. 把编码好的数据交给SVG滤镜
  context.putImageData(new ImageData(data, w, h), 0, 0);
  feImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', canvas.toDataURL());
  feDisplacementMap.setAttribute('scale', (maxScale / canvasDPI).toString());
};

这函数挺巧妙的,相当于个"翻译官":把数学逻辑(每个像素该移动多少)翻译成SVG滤镜能懂的"颜色语言"(红色=水平移动,绿色=垂直移动)。就像把中文翻译成英文,内容不变,表达方式不同。

到这里整个流程就通了!从数学公式到最终的视觉效果,每一步都有明确的目的。

让效果更自然:平滑过渡的秘密

前面代码里有个特殊函数 smoothStep,别看它不起眼,但这东西是让玻璃效果看起来自然的关键:

typescript 复制代码
function smoothStep(a: number, b: number, t: number): number {
  t = Math.max(0, Math.min(1, (t - a) / (b - a)));
  return t * t * (3 - 2 * t);  // 这个公式确保了平滑过渡
}

这个看似简单的公式 t * t * (3 - 2 * t) 其实大有讲究:它创造的不是匀速的线性变化,而是一种"慢→快→慢"的节奏感。想象一下汽车启动的过程------刚开始缓慢加速,中间快速提速,最后平缓到达目标速度。这种渐进式的变化让玻璃边缘的扭曲显得无比自然,而不是生硬的"开关式"跳跃。

整体实现思路总结

到这里,整个液体玻璃效果的实现路径就清晰了:

scss 复制代码
距离场SDF定义玻璃形状
          ↓
fragmentShader + smoothStep 计算每个像素该往哪移动
          ↓
updateShader 把数学结果编码成颜色 (R=水平移动, G=垂直移动)
          ↓
生成颜色贴图传给SVG滤镜
          ↓
feDisplacementMap 根据颜色指令移动像素
          ↓
CSS backdrop-filter 应用SVG滤镜
          ↓
背景文字被"玻璃"扭曲 (最终效果)

简单来说,就是:定义形状 → 计算移动 → 编码颜色 → 应用滤镜。每一步都在为下一步做准备,最终形成一个完整的处理链条。

相关链接

总结

回头看整个实现过程,思路其实挺清楚的:

  1. 问题起点:想精确控制像素移动,但CSS原生滤镜搞不定
  2. 技术选择 :用SVG的 feDisplacementMap 滤镜
  3. 关键难点:怎么生成滤镜要的"移动指令地图"?
  4. 数学建模:用SDF描述玻璃形状,定义扭曲规律
  5. 数据转换:把数学计算结果编码成颜色图片
  6. 应用渲染:实时生成位移贴图,驱动SVG滤镜
相关推荐
To_OC7 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic7 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志8 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble9 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯10 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司11 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g11 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森11 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http