
2026/04/29 20:46
コンポージティングとブレンド:ブレンドモードの背後にある数学的根拠と直感を解明する
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
合成(compositing)は W3C により「要素とその背景を組み合わせるプロセス」と定義され、Source Only、Backdrop Only、Both、Neither の 4 つの領域で発生します。これは Thomas Porter と Tom Duff の 1984 年の工作に由来し、公式 $C_o = F_s \times C_s + F_b \times C_b$ に基づく 12 つの Porter-Duff オペレータを用いて行われます。標準的な DOM 要素は常に「source-over」オペレータを使用しますが、HTML5 Canvas では
globalCompositeOperation を使用してモードを切り替えることができます。ブレンド(blending)は特定して「Both」領域(ソースと背景が重なる部分)で発生し、カラー関数を用いて色を変化させます。これらの関数は分離可能型(RGB)と非分離可能型(HSL)の 2 種類に分けられます。現代の表示装置は多くの場合 Display P3 を使用しており、一方 CSS のデフォルトは sRGB です。この不一致を適切に処理せず(color(display-p3 ...) または isolation: isolate を使用しない場合)、開発者は視覚的な不具合に直面するリスクにあります。テキストのイメージ上へのオーバーレイやデュトーンフィルター、メッセージバブルのようなカスタム UI 形状など、高度なエフェクトを実現するにはこれらの原理を習得することが不可欠です。本文
ブラウザはまさに奇跡です!このページに見える一画素ひとつひとつも、何百もの要素を積み重ね、数千万回にわたり「画素単位で結果はどうあるべきか」を決めて作り出されたものです。このプロセスには**合成(compositing)と合成モード(blending)**という 2 つの重要なステップがあり、今回の投稿ではこれらが何なのか、どのように機能し、ウェブ上で面白いエフェクトを構築する際にどう活用できるのかについて見ていきましょう。
学習目標:
- 合成とは何か、そしてそれが合成モードとどのような関係にあるか
- 合成モードとは何か、そしてその動作をどう捉えればよいか
- 存在する合成モードの種類と、現実的なシナリオで実用的なものはどれか
- CSS で合成モードを活用する方法と、直面しうる問題点
では、さっそく見ていきましょう!
合成(Compositing)
合成モードとは何なのか話す前に、まずは「合成」という概念について触れる必要があります。W3C の仕様によれば、合成とは**「要素をその背景(backdrop)と結合するプロセス」**を指します。これは何を意味しているのでしょうか?
あなたがこの文章を読んでいる間に画面に表示されている画素は、すべてあなたのブラウザによって描画されています。この Web ページは多くの HTML 要素から成り立つ多数のレイヤーで構成されており、ブラウザはそれぞれのレイヤーを描画してからそれらを結合しています。少し簡略化して言えば、以下のようになります:
[視覚的表現]
これらのレイヤーを互いに結合するステップが合成です。ここでは、合成時に描画対象となるレイヤーを**背景(backdrop)と呼び、描画されるレイヤーをソース(source)**と呼びます。
ご愛用の LLM ツールの入力テキストボックスのように、異なる要素から構成されたものがどのようにブラウザによって合成されるかを見てみましょう。ここでは各要素を「レイヤー」と仮定しますが、実際にはレイヤーには複数の要素が含まれています。レイヤーが描画されると後に変形やフェード操作を行う際も、再描画(re-painting)ではなく再合成(re-compositing)のみが必要となり、時間の節約とパフォーマンス向上に寄与します。
ここでは合成に関する一般論として、ブラウザにおける「レイヤー」の厳密な定義は本質的ではありません。スライダーを動かして合成ステップを確認したり、右上にある小さな 3D ボタンをクリックして異なる合成層を表示したりしてみてください。
[インタラクティブビジュアル]
この例では、単に現在のレイヤーを背景の上に描画して終わりにします。ブラウザにとってはこれでも十分です。しかし、合成はもっと強力な機能を備えています。文脈によっては、ソースレイヤーと背景を他の方法で結合することもできます。例えば、ソースを重ねずに背景の下に描画する、あるいは 2 つのレイヤーが重なっている部分(オーバーラップ)のみを描画するなどです。
可能な操作の種類を見ていきましょう。2 つの画像を結合する際、各画素は以下の 4 つの領域のいずれかに分類されます:
| 領域 | 名前 | 説明 | オプション |
|---|---|---|---|
| ソースのみ | Source Only | ソースにはあるが背景にはない画素。 | 2 通り:ソースを表示するか、何もしないかを選択 |
| 背景のみ | Backdrop Only | 背景にはあるがソースにはない画素。 | 2 通り:背景を表示するか、何もしないかを選択 |
| 両方 | Both | ソースにも背景にも存在する画素。 | 3 通り:ソース、背景、または何もしないかを選択 |
| どちらもない | Neither | ソースにも背景にも存在しない画素。 | 1 通り:何も表示しない(合成する必要がないため) |
全ての組み合わせをカウントすると、2 つの重なり合う画像の 4 つの領域に対する合成方法は計 $2 \times 2 \times 3 \times 1 = 12$ 通りあります。これらの 12 のオプションは、ルカスフィルムのトム・ポーターとトム・ダフが 1984 年に考案した**ポーターダッフ合成演算子(Porter-Duff compositing operators)**と呼ばれています。
これらは数学的に、合成時に 2 つの画素をどのように結合するかを定義しています。2 つのレイヤーを合成した際、画素の結果となる色を計算する式は以下の通りです:
$$C_o = F_s \times C_s + F_b \times C_b$$
- 変数: $C$ で始まる変数は色を表しており、3 次元ベクトル $(R, G, B)$ の形式で与えられます。
- $C_o$: 出力色(結果の色)
- $C_s$ と $C_b$: それぞれソースと背景の色
同様に、結果となる不透明度 $\alpha_o$ も以下の式で計算できます:
$$\alpha_o = F_s \times \alpha_s + F_b \times \alpha_b$$
ここで最も重要な点は、因子 $F_s$ と $F_b$ です。これらは使用する合成演算子によって設定され、合成操作の振る舞いを決定する核心となります。
例えば、ソースと背景とも色を持っている場合でも、その際のみソースの色を維持したい場合は、$F_s=1$、$F_b=0$ と設定します。すると色の式は $C_o = C_s$、不透明度の式は $\alpha_o = \alpha_s$ となり、結果としてソースの色のみが元の不透明度を保って残ることを意味します。
これらの概念を把握する上で最も大きかった助けとなったのは、これらを「ソース色と背景色の最大寄与度」として捉えることです。例えば $F_b=0$ とすると、背景色は一切寄与できません。一方、$F_b = 1 - \alpha_s$ と設定すると、その画素におけるソースの不透明度に応じて、背景色が結果に完全に寄与する可能性があることになります。
さきほど挙げた 12 の演算子について考え直すと、各演算子をそれぞれ $F_s$(ソースの最大寄与度)と $F_b$(背景の最大寄与度)で表現できます:
| 名前 | $F_s$ | $F_b$ | 説明 |
|---|---|---|---|
| Clear | 0 | 0 | イメージを消去 |
| Source | 1 | 0 | ソースのみが表示される |
| Backdrop | 0 | 1 | 背景のみが表示される |
| Source Over | 1 | $1 - \alpha_s$ | ソースが背景を覆う |
| Backdrop Over | $1 - \alpha_b$ | 1 | 背景がソースを覆う |
| Source In | $\alpha_b$ | 0 | ソースが交差部分で表示される |
| Backdrop In | 0 | $\alpha_s$ | 背景が交差部分で表示される |
| Source Out | $1 - \alpha_b$ | 0 | ソースが交差部分の外で表示される |
| Backdrop Out | 0 | $1 - \alpha_s$ | 背景が交差部分の外で表示される |
| Source Atop | $\alpha_b$ | $1 - \alpha_s$ | 背景の上に、交差部分だけが見えるソース |
| Backdrop Atop | $1 - \alpha_b$ | $\alpha_s$ | ソースの上に、交差部分だけが見える背景 |
| XOR | $1 - \alpha_b$ | $1 - \alpha_s$ | 交差部分は消去される |
元の式は少し混乱を招くため、実際に手を動かして遊ぶのがおすすめです。以下のプレイグラウンドでは、好きなポーターダッフ演算子を選択し、2 つの重なった正方形内の各画素において式がどのように振る舞うかを検証できます:
[インス펙タープレイグラウンド] 背景: rgb(245, 158, 11) | alpha 1.00 ソース: rgb(59, 130, 246) | alpha 1.00
不透明度とカバレッジ(Opacity and Coverage)
さきほど述べていましたが、合成式の一般的な $\alpha$(アルファ)変数は画素の不透明度を表しているように見えますが、それは不完全な記述です。実際には、$\alpha$ は**不透明度(opacity)とカバレッジ(coverage)**の両方を組み合わせた値となります。ここで、画素のカバレッジとは何かというと?
ブラウザが形状を描画する際、そのエッジはピクセルグリッドに完璧には合致しません:
[視覚的表現]
この場合、描画ステップでは形状が画素のどの部分を覆っているかを判定します。この割合をカバレッジと呼びます。色自体も完全に不透明ではない場合があるため、最終的なアルファ値 $\alpha$ は以下の式で与えられます:
$$\alpha = \text{opacity} \times \text{coverage}$$
つまり、形状のエッジにある完全な不透明度の画素でも、$\alpha$ 値は $1 \times 0.5 = 0.5$ となり、カバレッジが 1 である半透明(Opacity 0.5)の画素も同様に $\alpha = 0.5$ になります。結局のところ、$\alpha$ 値の起源が重要ではなく、合成式の計算において完全な不透明度であってもピクセルグリッドと完全に一致しない場合(例:文字や丸形のエッジ)、この式が適用されることを理解しておくことが重要です。
つまり、次回完全に不透明なフォントや丸い形状で滑らかなエッジを見た時、それは描画ステップがカバレッジに応じて背景色とソース色をフェードさせた結果であることがわかるはずです。便利です!
実践的な合成(Compositing in Practice)
以上が合成の仕組みです。しかし、これはあなたにとって何ができるのでしょうか?残念ながら、DOM 環境ではポーターダッフ演算子について直接制御することはできません。レンダリングは単に**ソースオーバー(source-over)**という合成方式を使用しており、これこそ最も直感的で期待される挙動ですが、仕様として固定されており変更できません。
一方、Canvas の世界ではすべてに発言権があります!キャンバスのコンテキストでは、
globalCompositeOperation プロパティを設定することで、新しい描画要素をキャンバスに重ねる際の合成演算子を切り替えられます。この機能を使えば、異なるポーターダッフ演算子を組み合わせ、前景の要素を描画し、必要であればマスク処理を行い、最後に背後の背景を描画するようなことができます。
これを説明するために、単純な形状と合成演算子だけで Pac-Man ゴーストを描画してみましょう。スライダーを動かしてゴーストが動き出す様子を見てください。各ステップで合成される次の形状は赤い破線で示されています。
- 頭部(サークル):ソースオーバー
- これには前述のいくつかの合成演算子が使用されます:
を使ってゴーストの「足」部分をくり抜き、最後に仕上げたゴーストの背後にピルと黒い背景を描画するためにdestination-out
を適用しています。ここでは創意工夫を楽しんでください;さらに面白いアイデアがたくさんあります!destination-over
- これには前述のいくつかの合成演算子が使用されます:
合成(Blending)
これで画面の画素がどのように合成されるか理解しました。では、どのように**混合(blending)**されるのかを見てみましょう。2 つのレイヤーを合成する際、合成はソースと背景が重なり合う領域(さきほどの「Both」)で行われ、ここで結果となる色を変化させることができます。
[図解:ブレンドはこの部分で行われる]
概念的には、混合ステップは合成ステップよりも前に起こります:その領域の各画素において、まずソース色が背景色とインプレース(その場で)混合され、新しいソース色が生成されます。その後、この新しい色が背景色と合成され、最終的な画素色が得られます。1 つの画素に対するこのプロセスを図示すると以下のようになります:
[図解:単一画素のプロセス]
ここでからは混合ステップに焦点を当てます。ソース画素と背景画素を混合する一般的な式は以下の通りです:
$$C_s' = (1 - \alpha_b) \times C_s + \alpha_b \times B(C_b, C_s)$$
注目すべき点は、$C_s$(ソース色)が式の両側に現れることです。つまり、後に合成される前に、ソース色は新しい値に置換されます。この新しく生成された色は、背景が完全に不透明でない場合は元の $C_s$ を、そして $B(C_b, C_s)$ が生み出すものを組み合わせます。これが何なのか?
$B$ は**混合関数(blending function)**と呼ばれます。これは 2 つの色を受け取り、新しい色を返すだけです。
理論的には、好きな $B$ の関数を何でも選べばよいのです。例えば「常にピンク色を返す」ような混合関数でも問題ありません。ただし実際には、我々は $B$ を自由に選択できません。代わりに、我々が使用するツール(この場合はブラウザ)からはいくつかの $B$ のプリセットが提供されており、そこから選ぶことになります。これらが以前遭遇したであろう一般的な合成モードであり、名前での参照:
lighten, darken などです。
つまり、合成モードは本質的には単なる関数の名前付けであることがわかりましたが、それに関連する質問が生じます:背後にある関数とは何か、そしてそれが実際に何をするのか?
チャネル分離(Channel Separation)
まず重要なことは、2 つ種類の合成モードが存在することです:(可分離型) と (非可分離型) です。
混合モードが各入力色のチャンネルに対して独立に動作する場合は、**可分離型(separable)**と呼ばれます。実際には、2 つの色を混合する場合、それぞれのチャンネルごとに別々に混合され(従って可分離)、その結果が結合されて最終色となります。
次のデモでこの概念を見ることができます:両方の入力色がそれぞれ R, G, B のチャンネルに分割され、得られる各チャンネルは対応する入力チャンネルのみに依存します。異なるチャンネル間への依存関係はありません。この例では、混合関数は単に各チャンネルの大きい値を選択します:
[デモビジュアル]
残りの全ての可分離型モードも同様です。それらが変わるのは、混合されたチャンネル値を決定するに使う混合関数だけであり、チャンネルが独立に混合されるというコンセプトは変わりません。
では**非可分離型(non-separable)はどうでしょうか?このカテゴリでは、結果となる色は RGB の各チャンネルごとに個別に混合して生成されません。代わりに、合成モードは色の高階層の知覚特性である彩度、彩度、明度(hue, saturation, luminosity)**に対して働き、これらを背景色とソース色間で混ぜ合わせ合わせて結果を生み出します。
ここでは高度な数学を扱っているわけではありません;具体的な式も存在しません。単に元の色の異なる成分を選択して最終色を得るだけです。このため、「非可分離」という用語は少し誤解を招くと感じます:RGB で考えると確かに正しいのですが(すべてのチャンネルが各知覚特性に寄与するため)、彩度・明度軸に沿って見ると実際には完全に分離可能です。より簡単な考え方は、合成モードがどのレベルで動作するかという視点です:我々は**「RGB チャネルベースのモード」と「知覚ベースのモード」**があります。
合成モードを視覚化(Visualizing Blend Modes)
これで合成モードの種類について理解がありますが、一般的にどんな作用を持つのかを直感的に把握するのはまだ困難です。正直に申し上げますが、この記事を読んだ後でも、合成モードが画像上でどのように振る舞うかを正確に予測することはできません。その理由は、人間の脳には各画素のすべてのチャンネルを同時に考慮するのは不可能だからです。
そこで、一般的な混合モードを扱うのを少し簡単にするために、単一画素に対する挙動を視覚化してみましょう。多くの状況下で、これにより期待される結果の大まかな方向性を推測する助けになります。
まずは色の復習をします:RGB では各チャンネルの値は 0 から 1(含)の範囲を取ります。全ての画素はこれらの 3 つの成分から成り、画像を分割して各チャンネルごとに確認できます:
- 元の写真
- 赤
- 緑
- 青
明るいチャンネルを持つ領域は、その色がその領域で大きく寄与していることを意味し、暗い色はその色が存在しないことを示します。ここで「暗い」というのは RGB チャンネルの値を指し、人間の知覚では全緑の方が全青よりも明るく見えることが多いという点に注意してください。これらを合成すると元の写真が得られます:
[合成画像]
3 つのチャンネルがありますが、各チャンネルで同様に動作するため、理論的には 1 つのチャンネルでのみ動作を確認すれば他の 2 つも推測できるはずです。
ただし、特定のチャンネルを可視化として選択する代わりに、グレースケール値を汎用的なチャンネル表現として使用します。値 0(黒)はチャンネルが最終色に全く寄与せず、1(白)は完全な寄与を示します。
[グレースケール可視化]
このバーは、1 つの色に対して 1 つずつチャンネルを表示しています。この概念を混合モードに拡張すると、2 つの色を結合するものなので、横軸が背景のチャンネル値、縦軸がソースのチャンネル値を表す二次元平面プロットが可能になります。これにより、単一チャンネルでの動作を視覚化できますが、各チャンネルで同じように動作することから、単一の汎用的な可視化だけで十分です。
例えば、$B(C_b, C_s) = \max(C_b, C_s)$ の混合モード(常に比較する中で大きい方、すなわち明るい方の値を選択するもの)をプロットしてみましょう:
[図解]
結果はほぼ全体的に明るい色となり、合成モード適用後の全体像が明るくなることを示しています。さらに詳しく見ると、どちらか一方でも光源または背景が明るい値であれば、結果も明るい値となります。最後に、対称性から背景とソースの順序が結果に影響しないこともわかります。他のいくつかの合成モードは非対称であり、ソースと背景の順序によって異なる振る舞いをするためです。
図にマウスカーソルを合わせると、計算に使用される背景・ソースの値と結果となる値が表示されます。ここでの仕組みをじっくり理解してください;後続で同じスタイルの図を用いて他の合成モードも調べる予定です。
新しく学んだ可視化手法を適用し、この合成モードが画像上でどのように動作するか見てみましょう。画像を固定されたレイヤーと混合し、画像の明るさと混色する色の相互作用を確認します。再びグレースケールに限定するのは、フルカラー画像で色がどう変化するか不安になる必要がないためです。
[画像混合デモ]
これにより、この式が何をやるのかの良い直感が得られるはずです:暗いチャンネルは混色された値のチャンネル値まで引き上げられ、明るいチャンネルは不変のままです。ここで探った式は
lighten 合成モードで使われているものであり、残りのモードを見ていきましょう!
合成モードのカテゴリ分類(Categorizing Blend Modes)
CSS の異なる合成モードを見ると、与えられた 2 つの色に対する効果に基づいてカテゴリ分けできます。例えば、
lighten という名前通り全体を明るくする傾向がありますが、これだけの効果を持つものは他にもあります。
変更を生じない
normal を無視し、以下のようなグループに分けます:
- 明るくする(Lighten): 画像を明るくする(
,lighten
,screen
)color-dodge - 暗くする(Darken): 画像を暗くする(
,darken
,multiply
)color-burn - **コントラスト(Contrast)
overlay: 明るくする/暗くするの組み合わせ(
soft-light,
hard-light`), - 反転(Inversion): 類似度に基づいた効果(
,difference
)exclusion - 成分操作(Component): 非可分離型 / HSL ベース(
,hue
,saturation
,color
)luminosity
それぞれのカテゴリを少し掘り下げ、動作を確認しましょう。
Lighten
画像を明るくするモードから始めます。最も単純なものは既に見た通りで、同名の
lighten モードが各 RGB チャンネルで大きい(=明るい)値を選択し、結果としてより明瞭な画像となります。
特に注意すべきは、
lighten は常に入力 2 つのうちどちらか一方のみを選択するということです。したがって、2 つの暗い層を合成しても、結果はあくまでそれらのうち明るい方のままです。グラフに見られる鋭い対角線の原因はこの点にあります(2 つの同等な値の場合、結果は同じ値となります)。
より滑らかな結果を得るには、
screen モードを使用できます。これも明るくする効果がありますが、勝者を決定する代わりに滑らかに混合します。写真画像では screen が自然な選択ですが、ロゴのようなグラフィック合成でカラーの補間を避けたい場合は lighten も有用です。
最後に
color-dodge は、2 チャンネルの和が 1 に近づく際に非常に鋭い遷移エッジを持ちます。ほぼ半分が完全に白になるため、非常にアグレッシブな明るくする効果となり、結果が急激に明るくなります。
Darken
lighten カテゴリの各モードに対し、暗くするカテゴリにも補完的なオプションが存在します。順序通り darken, multiply, color-burn です:
[視覚比較]
darken は各チャンネルを暗くしますが、lighten と同じ「エッジ」の問題を抱えているため、画像用にはより滑らかな遷移を持つ multiply がほぼ常に好まれます。color-dodge の補完は color-burn で、ソースがより暗いほどチャンネルを劇的に暗くします。
Contrast
コントラスト系モードは興味深く、1 つのチャンネルに基づいて
lighten または darken の効果を適用します。overlay は screen と multiply の組み合わせで、背景色が決める因子となります。背景が暗い(< 0.5)場合は結果を暗くし、それ以外は明るくするものであり、両方が半分の強度で適用されるため遷移がやや滑らかです。
[Overlay / Hard Light / Soft Light ビジュアル]
hard-light は基本的にソースと背景が入れ替わった overlay で、同じスクリーン/マスキュリティーの組み合わせですが、今度はソースチャンネルが明るくするかどうかを決定します。暗いソースは結果を暗くし、明るいソースは結果を明るくします。
最後に
soft-light は概ね overlay と同様ですが、遷移が滑らかです。多くのケースで良好な代替手段ですが、使用カラーによっては異なる結果になる可能性があります。
Inversion
反転系モードは、適用される 2 つのチャンネルを比較し、類似度に基づいて結果を選択するものとして考えられます。
difference は入力チャンネルが等しい場合に暗い結果を与え、異なるほど明るくします。
exclusion は柔らかいバージョンの差であり、入力チャンネルの極端な値(0 または 1 に近い)では同じ挙動しますが、両方が 0.5 に近い場合でも類似値にもかかわらず黒ではなく中程度の明るさになります。
自ら探究せよ(Explore yourself)
理論上の動作は理解しましたが、いまだに実際の画像でのテストを行っていません。その時が来ました!まず、これまでに使ったグレースケール画像上で全ての合成モードの動作を確認します。チャートを見直して挙動を予測してみてください:
- Lighten
- Screen
- Color Dodge
- Darken
- Multiply
- Color Burn
- Overlay
- Hard Light
- Soft Light
- Difference
- Exclusion
最後に、理論に色を追加しましょう。グレースケールに留めたのは画像内のカラー挙動が予測困難なためですが、個々の画素については見ていきます。以下のデモでは、好きな 2 つの色と合成モードを選択し、得られる結果を確認できます:
[インタラクティブカラーピッカー]
ここにも混合モードの数学側面を隠しています:各モードの式を検視し、どのように適用されているか確認できます。
[式検視ツール]
ブラウザでの使用(Usage in the browser)
これまでに全ての合成モードと結果を知った今、 finalmente 活用します!
mix-blend-mode または background-blend-mode プロパティを使用します。
要素混合には単に
mix-blend-mode を使い、上で議論した 16 の合成モードのいずれかを渡します。ブラウザが残りの合成と機能適用を処理してくれます:
.element { mix-blend-mode: multiply; }
別の方法は背景の混合です。複数の背景値に対して
background-blend-mode プロパティに指定することで、階層化されたエフェクトを作成できます(1 つの背景ごとのモードを積層):
.element { background: url("..."), url("..."), hotpink; background-blend-mode: multiply, lighten, normal; }
これでお好みのブラウザで合成モードを使用できるようになります!
カラー空間の工夫(Fun with Color Spaces)
いや待って...
ええ、ご期待に添えず申し訳ありませんが、落とし穴があります。ブラウザでの合成モードは、正確な結果を得ることに依存すると苦痛になるわずかな詳細が存在します。説明のため、完全に赤い円と完全に緑の円を重ね合わせ、最小値を取る
darken モードを適用しましょう。
[赤円 (1,0,0)] [緑円 (0,1,0)] (あなたのディスプレイは P3 カラーガマートをサポートしないため、ここでは正しく黒が表示されます。想像してください :))
純粋な数学だけであれば、重なり領域の値は $(0,0,0)$ なので黒であるはずです。しかし、2016 年以降の MacBook で Chrome や Safari で閲覧している場合、...茶色?を見ますよね??
これはなぜ起こるかというと、合成モードはユーザーディスプレイのカラー空間内で適用されるためです。CSS 色はデフォルトで sRGB で指定されますから、もしあなたのディスプレイがそれに一致すれば完璧です。計算はすべて正確になります。しかし一部のディスプレイ(現代の MacBook など)は、sRGB よりも多くの色を表現できるDisplay P3というより広いカラー空間をサポートしています。そのため sRGB で色を指定しても、ブラウザはまずそれを P3 に変換します。
ここで問題が発生します:sRGB は Display P3 に対し線形的にマッピングされていないため、sRGB カラー空間は奇妙な形ですぐさま埋め込まれています。純粋な赤・緑・青が P3 でどうなるか確認してみてください:
- RGB カラー (1.000, 0.000, 0.000) → P3 カラー (0.917, 0.200, 0.139)
- sRGB の純粋な緑は P3 で $(0.458, 0.985, 0.298)$ に変換される。
これにより計算が乱されます。0 を掛けるつもりだったが、実際にはほぼ 0.5 を掛けていたのですぐにわかります。さらに酷なのは、Firefox はディスプレイのカラー空間に関係なく sRGB で混合を実行するため、ブラウザごとに結果が異なることです!
スタイル目的でのみ合成モードを使用する場合は問題ありません。しかし精度が必要な場合は、ブラウザが変換する必要がないように P3 カラーを直接指定できます。これには CSS の
color() 関数を使います。完全な緑を得るには以下のようにします:
background-color: color(display-p3 0 1 0);
これで計算が正しくなり、P3 空間においても適切に混合された色を得られます。やった!
分離(Isolation)
CSS で合成モードを使用する場合、時には次のような問題に直面します:背景の一部とは混合したいが、他の部分とはしたくない場合です。古典的な RGB ベン図の例が良いでしょう:
[視覚表現] 背景と混合する...
この図では円同士は混合したいですが、パネル背景とは絶対にしてはいけません。幸いにも CSS は**スタッキングコンテキスト(stacking contexts)**というソリューションを提供しています。ちょうどこの解説が終わろうとしたら、まだ別の箱をひっくり返すようなことになったのはご承知の通りです...
大まかには、スタッキングコンテキストは一部の DOM 要素をグループ化し、重要な特性として「同じコンテキスト内の要素のみが混合可能」という制限を持ちます。つまり、上記のベン図を構成する要素を新しいスタッキングコンテキストにグループ化できれば、それらの要素はスタッキングコンテキスト外部(例:パネル背景)と混合されません。素晴らしい!
isolation プロパティを auto(デフォルト)から isolate に設定することで、手動で新しいスタッキングコンテキストを作成できます。このプロパティを設定した要素自体はスタッキングコンテキストの一部となるため、その要素には背景色を持たせておかないと注意が必要です。
残念ながら、CSS には不透明度を指定する
opacity プロパティのように、明示的に新しいスタッキングコンテキストを生じさせる他の多くのプロパティが存在します。これについて詳しく掘り下げるよりも、素晴らしい Josh Comeau が既に完璧に説明した資料をご紹介します(関連記事をご参照ください)。Josh、ありがとう!
現実的な応用例(Real world applications)
このセクションは最も難しい部分だと思います。私は混合モードを頻繁に使用するわけではありませんが、その背後にある仕組みを理解するためにこの記事を書きました。しかし、いくつかのケースで非常に役立つ事例があり、それらに焦点を当てたいと思います!
画像上のテキスト
よく見るシナリオは、画像の上にテキストを重ねることです。一般的な解決策はテキストを半透明にすることですが、これではテキストが乗る領域だけが暗く・明るくなるだけで、地味に見えます。混合モードを使えば、背景の色を維持しつつ選択的にテキストと画像を統合できます。特に
overlay モードがこの用途に非常に適しています!
以下の画像で固体・半透明・混合されたテキストの間を切り替えてご確認ください:
[サントリーニ画像比較:固体 | 透明度 | 混合]
ここではさらにシャープなテキストシェードを追加し、カットアウト 3D エフェクトを生み出しています。この手法は大きな太字のテキストには最適ですが、すべてのシナリオに適しているわけではありません。ただし必要時によくあるトリックです!
画像エフェクト
混合モードはテキストだけでなく、画像上にも楽しい効果を作成するために使用できます。CSS だけですべてを達成可能です。
例えば、明るい部分のある色に暗くされ、暗い部分のある色に明るくされることで、2 つの色を追加しデュートーンエフェクトを生み出します:
[元画像 | シングルトーン | デュートーン]
これは Una Kravets 氏が最初に見つけ、彼女のウェブサイトに他の合成モードの画像効果に関する素晴らしいコレクションをまとめています(検索語は"CSS Image Effects")です。多数の画像を統一したい場合や、異なるスタイルや照明条件下で整合させたい場合に特に強力な機能です。必ずしもここのように劇的なエフェクトである必要はありませんが、小さなエフェクトでも複数の画像のスタイルを統合するのに効果的です。
歪んだ境界
CSS では丸い・角ばった輪郭は容易ですが、それより複雑な場合は工夫が必要です。iOS のメッセージバブルのような形への輪郭線やリングを追加する方法はどうでしょうか?
[図解:メッセージバブル]
読者諸氏、長々お読みいただきありがとうございます!
少しズルをして SVG を使用することも可能ですが、メインのバブルを HTML 要素として残したいです。角のエフェクトのために小さな SVG を使用します。これらにリングを追加すると、常に一方が他方を覆います。2 つの円の例でわかりやすく示します:
[図解:2 つの重なった円]
しかし上の円に
lighten モードを適用する場合は、背後の円の境界線が重なって消え、単一の連続的な輪郭線が残ります。境界線はほぼ常に周囲形状よりも一貫して明るく(または暗く)なるため、これはあらゆる輪郭線のための素晴らしい技術です。素敵ですね!これをメッセージバブルに適用してみましょう:
[図解:ストロークと輪郭の混合 | 混色ストローク]
実際の応用ではより控えめな境界スタイルを選ぶでしょう。ただしデモ目的には最適です。この手法が特に有用なケースはツールチップの矢印スタイル付けなどです。どこでも使用されていることは少见なので、比較的新しい技術かもしれません。ぜひ有益だと思います!
ブロブと楽しいエフェクト
最後に紹介するエフェクトは混合モードだけでなくフィルタも大きく使用しており、この記事では議論していませんでした。それでも紹介したいのは、最初に知った時「これが CSS だけでできるの?」という驚きがあったためです。
[図解: Blur Contrast Blend overlay]
このエフェクトは、要素を大きくぼかしてコントラストを上げることで黒いブロブ効果を得ます。ただし黒いブロブ以外の場合は半分の段階までしか進みません。しかし、目的の画像や色を重ねて
lighten モードを適用することで、自ら前景を黒いブロブに強制できます。
非常にクリエイティブなアプローチだと思います!誰が最初にこの技術を考案したのかはわかりませんが、ご存知の方はクレジットのためにご連絡ください。フィルタと混合モードを組み合わせることで他にも多くのクリエイティブなエフェクトが可能で、最近 Bluesky で共有したエフェクトも同様の概念を使っています。仕組みをご理解いただけると幸いです!
終わりのない可能性(Endless possibilities)
ここに記載されていない素晴らしいことがたくさんあるはずです。共有に価値があるケースを知っている場合はお知らせください;私はそれをここに追加し、あなたへのクレジットを付けさせていただきます!
さらなる読書(Further reading)
このほど長距離を辿っていただき、ありがとうございました。合成と混合についての私のアプローチを楽しんでいただけたことを願っています。常に学ぶ余地があるので、終わりにして、さらに多くの情報を提供するいくつかのリソースを紹介したいです:
- この記事で導入した合成の数学は簡略化されています:ブラウザは実際にはpremultiplied alphaという形式で色を格納しており、この記事では触れていない多くの利点があります。このテーマについて詳しく知りたい場合は、Bartosz Ciechanowski の Alpha Compositing が素晴らしい読書になります!
- 合成の起源について学び、歴史レッスンと組み合わせたい場合は、オリジナルの 1984 年の論文(Thomas Porter と Tom Duff)も読む価値があります。
- 最後に、ブラウザがどのように合成・混合を処理するかについてのより技術的な説明については、W3C の仕様書は思っているほど恐ろしくありません。