ReactでLiquid Glassを完全再現する

Appleから発表されたLiquid Glassを、html-to-imageによるDOM画像化と極座標変換によるピクセル操作を用いてReact上で完全再現

イントロ

WWDC25でAppleからOSをまたいだ大規模なUIアップデートとして、「Liquid Glass (流体グラス)」が発表されました。これはいままであった単なるぼかしやすりガラス(フロストガラス)エフェクトではなく、AppleのUI研究チームが実際に現実のガラスを用いて光の屈折などを研究し、それをソフトウェアに落とし込んだものです。そのため、圧倒的なハイクオリティでリアルなデザインとなっています。

この美しいUIデザインはSwiftUIやUIKitといったAppleのネイティブAPIでしか使うことはできないため、Webアプリでこのリッチな表現を使うには自前で実装する必要があります。

ここでは、ReactコンポーネントとしてLiquid Glassを完全再現するまでの流れと、その実装アプローチについて記録します。

デモ

まずは以下のデモで、実際の再現版Liquid Glassの質感(ドラッグやピンチ操作での変化)を体験してみてください。

実装

多くは、要素の背景に画像的な加工を施すにはCSSの backdrop-filter やSVGフィルターを使いますが、それらは単なるぼかしや単純なピクセルの移動しか行えません。ピクセルを立体的に拡大縮小、歪み、さらには「色収差」を発生させるLiquid Glassの表現にはたどり着きません。

これをWeb上で実装するため、「DOMをすべて画像化し、その画像をCanvas上で直接ピクセル加工して切り出す」というアプローチでガラス効果を発生させています。

細かな処理フローは次のとおりです。

  1. DOMのCanvas化: html-to-image (または html2canvas) を用いて、対象となるHTMLのDOM要素すべてをCanvas化する。
  2. 全体スクリーンショットの取得: Canvasの全体スクリーンショットをメモリ上に保持する。
  3. 対象領域の切り出し: スクリーンショット画像から、ガラスオブジェクトが重なっている部分(例:ナビゲーションバーなど)だけを切り出す。
  4. 屈折ピクセルの計算(極座標変換): 切り出した画像データ(ImageData)から、ガラスの立体感を持たせるため、中心からの距離を極座標的に計算する。
  5. 距離の圧縮と反転: 屈折率や厚みをシミュレートするため、距離を関数で圧縮・反転させ、直交座標に戻す。
  6. ピクセルの再配置と色収差の付与: 新しい座標の画素をサンプリングして再配置する。同時に、RGBの各チャンネルをずらして色収差(Chromatic Aberration)を発生させる。

これにより、ただの平面ではない「立体的なガラスの曲面」と「リアルな屈折」をピクセル転置で実現しています。

コア実装

以下が、ガラスの質感や厚み、色収差を計算してピクセルを転置させるコア部分のコードの抜粋です。Canvasから取得した ImageData をもとに、ピクセルごとに屈折をシミュレートしています。

// ガラス効果を適用する領域の全ピクセルをループ
for (let y = 0; y < size.y; y++) {
    for (let x = 0; x < size.x; x++) {
        // ... (中心点からの距離 distSq などを計算) ...
        const distance = Math.sqrt(distSq);

        // 1. 距離の圧縮(曲面的な屈折のシミュレート)
        const ratioInRing = (distance - innerRadius) * ring.invRingThickness;
        const compressedRatio = Math.pow(ratioInRing, compressionPower);
        const newDistance = innerRadius - compressedRatio * (innerRadius - ring.reflectMinRadius);

        // 2. 距離の変化率をもとに、元画像の参照先(dx, dy)をずらす
        const ratio = newDistance / distance;
        const baseDX = dx * ratio;
        const baseDY = dy * ratio;

        // 3. 辺の広がり (Spine Distortion) の計算
        // カプセル型の長軸(Spine)からの距離に応じて、端に行くほど歪みを大きくする
        const dxMid = cx - spineMidX;
        const dyMid = cy - spineMidY;
        const spineRatioX = dxMid * invHalfSpineLength;
        const spineRatioY = dyMid * invHalfSpineLength;
        const spineForce = -maxSpineDistortion * Math.pow(ratioInRing, 3);
        const spineShiftX = spineRatioX * Math.abs(spineRatioX) * spineForce;
        const spineShiftY = spineRatioY * Math.abs(spineRatioY) * spineForce;

        const finalBaseDX = baseDX + spineShiftX;
        const finalBaseDY = baseDY + spineShiftY;

        // 4. 色収差 (Chromatic Aberration) の付与
        const aberForce = maxAberration * Math.pow(ratioInRing, 4);
        const shiftX = finalBaseDY * aberForce;
        const shiftY = finalBaseDX * aberForce;

        // R, G, B で参照するピクセル座標を別々にずらす
        const srcXR = (cx + finalBaseDX - shiftX + 0.5) | 0;
        const srcYR = (cy + finalBaseDY + shiftY + 0.5) | 0;
        const srcXG = (cx + finalBaseDX + 0.5) | 0;
        const srcYG = (cy + finalBaseDY + 0.5) | 0;
        const srcXB = (cx + finalBaseDX + shiftX + 0.5) | 0;
        const srcYB = (cy + finalBaseDY - shiftY + 0.5) | 0;

        // 4. 新しいImageDataへRGBごとにピクセルを書き込み
        if (srcXG >= 0 && srcXG < size.x && srcYG >= 0 && srcYG < size.y) {
            // ... (各チャンネルごとのピクセル代入処理) ...
        }
    }
}

課題 / パフォーマンスチューニング

フローを見て分かる通り、全DOMの画像化とCanvas上でのピクセル単位のループ処理が発生するため、かなり高負荷な処理になります。 特にReactのような随時要素(State)が変動するフレームワークとCanvasのスクリーンショットは相性が悪く、スクロールや画面の変化を検知するたびに再撮影する必要があります。

実際の実装(例: SchoolSchedule アプリの Bottom コンポーネント)では、以下のような工夫をしてパフォーマンスを維持しています:

  • スクロールやスワイプなどの激しいアニメーション中(isMovingなど)は一時的にキャプチャを止め、操作が終わった後に再描画する。
  • requestAnimationFrame や定期的な更新検知(setInterval でのDOM高さ監視など)を組み合わせ、変更があった場合のみDOMの画像化を走らせる。
  • 画像化の際に不要な再描画を抑えるため、キャッシュを活用する。

スクショを撮るためのオーバーヘッドが大きいので、ここをさらに削減できると高速な処理が期待できます。

質感を高める追加処理

Liquid Glassは単純なピクセルの歪みだけでなく、「色収差」や「辺の広がり」などの追加処理が必要です。実際のコンポーネントでは以下のように対応しています。

  • 色収差 (Chromatic Aberration): 光がガラスを通る際の波長による屈折率の違いを再現。RGBを分離し、Redチャンネルはプラス方向へ、Blueチャンネルはマイナス方向へ数ピクセル移動させて描画します。
  • 辺の広がり: 辺の中央を原点に、端に行くほど移動量が大きくなるようなカーブ(三次関数など)を用いて、物理的なガラスの厚みを表現します。

このコンポーネントでは変数を調整して、自由にガラスエフェクトを操作可能にしています。主なパラメータは以下の通りです:

  • 縦横カスタムサイズ (Width / Height): ガラスオブジェクトのサイズ。
  • サンプリング距離 / Inner Radius: オブジェクトの立体感(カーブの始まり)を演出。
  • 屈折距離 / Compression Power: オブジェクトの立体曲面感・屈折の強さを演出。
  • 色収差レベル / Max Aberration: ガラスの厚みによるRGBのズレ強度を演出。

アウトロ

ピクセルを直接操作することで、WebアプリでもAppleのLiquid Glassを限りなく近い形で再現することに成功しました。Webでも圧倒的に美しいUIを作ることができます。 ただし、実際のApple APIの内部実装と完全に一致しているわけではなく、実用面では(特に複雑なDOM構成の場合)動作が低速になる可能性があるため、パフォーマンスの最適化やWebGL/ピクセルシェーダーへの置き換えなど、まだまだ改善の余地はあります。

この内容のWebアプリ: Liquid Glass React
この内容のリポジトリ: GitHub - rsu-Suba/Liquid-Glass-React