浏览器中的 Liquid Glass:用 CSS 与 SVG 实现折射效果

572 阅读3分钟

引言

苹果在 2025 年 6 月的 WWDC 上引入了 Liquid Glass 特效 —— 一种让界面元素看起来像由弯曲、折射玻璃制成的惊艳 UI 效果。本文将带你在 Web 上用 CSSSVG displacement map 和基于物理的折射计算,重现类似的效果。

本文并不追求像素级还原,而是以核心的 折射高光(specular highlight) 为目标,作为可扩展的实验性原型。

我们会从光线折射的物理原理讲起,再逐步搭建这个效果。

⚠️ 当前交互 demo 仅能在 Chrome 中运行(因为涉及 SVG filter 的 backdrop-filter)。 其他浏览器也能阅读文章并与部分内联模拟交互。


理解折射

折射是指光从一种介质进入另一种介质时改变方向的现象(例如空气进入玻璃)。这是因为光在不同介质中传播速度不同。

其规律由 Snell–Descartes 定律描述:

n1sin(θ1)=n2sin(θ2)n_1 \sin(\theta_1) = n_2 \sin(\theta_2)
  • n1n_1:入射介质的折射率
  • θ1\theta_1:入射角
  • n2n_2:折射介质的折射率
  • θ2\theta_2:折射角

关键点:

  • $n_2 = n_1$,光线直线穿过,不发生折射。
  • $n_2 > n_1$,光线向法线方向偏折。
  • $n_2 < n_1$,光线远离法线偏折,且在一定条件下会发生 全反射(Total Internal Reflection)
  • 当入射光垂直入射时,不论折射率如何,光都会直线通过。

本项目的简化条件

为减少复杂性,本文限定如下:

  • 环境介质为空气(index=1)。
  • 使用折射率大于 1 的材料(典型值 1.5,即玻璃)。
  • 只考虑一次折射事件(忽略出射时的二次折射)。
  • 入射光始终垂直于背景平面(无透视)。
  • 物体为与背景平行的二维形状。
  • 无物体与背景的间隙。
  • 本文只使用圆形,延伸到其他形状需额外计算。

在这些假设下,每条光线的折射方向都能通过 Snell’s Law 唯一确定。


构建玻璃表面

玻璃表面由一个函数定义,用于描述从边缘到中心的厚度变化。

const height = f(distanceFromSide);

入射角由曲面的切线(即函数的导数)决定:

const delta = 0.001;
const y1 = f(distanceFromSide - delta);
const y2 = f(distanceFromSide + delta);
const derivative = (y2 - y1) / (2 * delta);
const normal = { x: -derivative, y: 1 };

曲面函数示例

本文使用四种不同函数,展示曲面对折射的影响:

  1. Convex Circle

    y=1(1x)2y = 1 - (1-x)^2

    简单圆弧 → 球面。转折较硬,折射边缘明显。

  2. Convex Squircle

    y=1(1x)4y = 1 - (1-x)^4

    Apple 常用的 Squircle。过渡更平滑,折射渐变自然。

  3. Concave

    y=1Convex(x)y = 1 - Convex(x)

    碗状凹面,折射光线向外发散。

  4. Lip

    y=mix(Convex(x),Concave(x),Smootherstep(x))y = mix(Convex(x), Concave(x), Smootherstep(x))

    外凸内凹,适合开关滑块效果。


位移向量场(Displacement Vector Field)

折射后的光线相对原位置的偏移,构成一个向量场。 在圆形中,偏移方向始终垂直于边界。

为了减少计算量,我们只需计算单个半径上的 127 个采样点,再旋转复制到整个圆形。


向量归一化与编码

SVG displacement map 需要每个像素的偏移量映射为颜色。

  1. 归一化:先计算最大位移,然后将所有向量缩放到 [0,1][0,1]

  2. 转换为颜色

    const x = Math.cos(angle) * magnitude;
    const y = Math.sin(angle) * magnitude;
    
    const result = {
      r: 128 + x * 127,
      g: 128 + y * 127,
      b: 128,
      a: 255
    };
    

红色通道代表 X 轴偏移,绿色通道代表 Y 轴偏移。


SVG Displacement Map

SVG <feDisplacementMap /> 负责应用位移:

<svg colorInterpolationFilters="sRGB">
  <filter id="liquidGlass">
    <feImage href={displacementMapDataUrl} width={w} height={h} result="map"/>
    <feDisplacementMap
      in="SourceGraphic"
      in2="map"
      scale={maximumDisplacement}
      xChannelSelector="R"
      yChannelSelector="G"/>
  </filter>
</svg>

scale 参数决定了偏移强度,可动态调整实现动画。


高光(Specular Highlight)

真实玻璃有亮边。我们用一个 rim light 效果模拟,基于表面法线与光源方向的角度生成亮度。

最终效果通过 <feBlend /> 将高光与折射层叠加。


Chrome 限制

  • Chrome 支持将 SVG filter 作为 backdrop-filter
.glass-panel {
  backdrop-filter: url(#liquidGlass);
}
  • 其他浏览器不支持,只能退化为普通滤镜或模糊。

UI 组件示例

  • 放大镜(Magnifying Glass):两层 displacement map,实现边缘折射+中心放大。
  • 搜索框(Searchbox):Convex 玻璃框,折射背景。
  • 开关(Switch):Lip 曲面,外凸内凹,中心缩小,边缘折射。
  • 滑块(Slider):Convex 边框,背景随滑动折射。
  • 音乐播放器(Music Player):模仿 Apple Music Liquid Glass 面板。

结论

本文原型将 Apple 的 Liquid Glass 效果简化为:

  • 实时折射(refraction)
  • 加 rim light 高光

该方法目前仅限 Chromium 内核(Chrome/Electron 等),其他环境可用模糊模拟替代。

但其可扩展性强,适合做 UI 探索。


Vue3 + Vite 的最简 demo,可以直接跑起来看到 “Liquid Glass 折射效果”。 因为浏览器限制,目前只能在 Chrome 中使用(依赖 backdrop-filter: url(#filterId))。

1. 新建项目

在终端运行(推荐用 pnpm/yarn 也可以):

npm create vite@latest liquid-glass-demo -- --template vue
cd liquid-glass-demo
npm install

2. 写一个 LiquidGlass.vue 组件

src/components/LiquidGlass.vue 中新建文件:

<template>
  <div class="wrapper">
    <!-- 背景 -->
    <div class="background">
      <div v-for="i in 40" :key="i" class="bg-line"></div>
    </div>

    <!-- 应用 Liquid Glass 滤镜的面板 -->
    <div class="glass-panel">Hello Liquid Glass</div>

    <!-- SVG Filter 定义 -->
    <svg style="display: none">
      <filter id="liquidGlassFilter">
        <!-- 生成一个位移贴图(这里用简单噪声图代替预计算的 displacement map) -->
        <feTurbulence type="turbulence" baseFrequency="0.05" numOctaves="2" result="turb" />
        <feDisplacementMap
          in="SourceGraphic"
          in2="turb"
          scale="20"
          xChannelSelector="R"
          yChannelSelector="G"
        />
      </filter>
    </svg>
  </div>
</template>

<script setup>
// 没有 JS 逻辑,全部在 CSS + SVG 里
</script>

<style scoped>
.wrapper {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
}

.background {
  position: absolute;
  inset: 0;
  display: flex;
  flex-wrap: wrap;
}

.bg-line {
  width: 10%;
  height: 10%;
  background: linear-gradient(135deg, #ff7eb3, #65d6ce);
}

.glass-panel {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 300px;
  height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;

  color: white;
  font-size: 20px;
  font-weight: bold;

  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.4);

  /* 核心:使用 SVG filter 作为 backdrop-filter */
  backdrop-filter: url(#liquidGlassFilter);

  /* 背景半透明,配合玻璃质感 */
  background: rgba(255, 255, 255, 0.15);
}
</style>

3. 在 App.vue 中使用

修改 src/App.vue

<template>
  <LiquidGlass />
</template>

<script setup>
import LiquidGlass from './components/LiquidGlass.vue'
</script>

4. 启动项目

npm run dev

打开 Chrome 浏览器,访问 http://localhost:5173/,就能看到一个半透明玻璃面板,带有液态折射效果。


⚠️ 注意:

  • Safari / Firefox 不支持 backdrop-filter: url(#filter),只能看到普通半透明玻璃。
  • 如果要实现文中提到的 基于 Snell’s Law 的真实 displacement map,需要在 JS 中生成图像并传给 <feImage />,这是下一步的增强。