freeeの開発情報ポータルサイト

CSSのmix-blend-mode完全に理解したい

こんにちは、デザイナーだけどFigmaよりTypeScriptを触っている時間のほうが長いid:ymrl です。

この記事はfreee Developers Advent Calendarの2日目です。

CSSのmix-blend-mode完全に理解したい

CSSの mix-blend-mode 、なんだかカッコいい見た目のものを作るのに便利そうとずっと思っていたものの、 mix-blend-mode に何を指定したらどうなるのか想像しづらく、私は「当てずっぽうでいろんな値を入れてみていい感じになったのを使う」みたいな状態でずっと誤魔化していました。

なにせ MDNのmix-blend-modeのページに載っているサンプルがこれですよ。

mix-blend-modeの効果を示したサンプル。赤・緑・青の楕円が重ねられて、値ごとに様々な効果が発生している
MDNに載っているサンプル(MDNのmix-blend-modeのページより引用)

これだけ見てもよくわからない!

ということで、それぞれの blend-mode の値のときの色の計算方法を調べて、自分で計算してみれば mix-blend-mode と和解できるんじゃないかと思ったのです。

mix-blend-mode計算機を作った

mix-blend-modeで何が起きるかを理解するために、Compositing and Blending Level 2のEditor's Draftを参考に、いきなりコードを書きました。

https://codepen.io/ymrl/pen/qBKyBrR

See the Pen Untitled by ymrl (@ymrl) on CodePen.

指定したblend-mode での backdrop color 上で source color が何色になるかを計算して、CSSで指定した色と並べてくれます。

hard-light で #2864f0と#e9e7e7 を指定し、#49b5fc が得られている様子

こういうツールを自作したことで、mix-blend-modeについてそこそこ理解できるようになってきました。

mix-blend-modeはディスプレイのカラープロファイルの影響を受ける

ツールの作成中にいきなり悩まされたのが、mix-blend-modeで作られる色はディスプレイのカラープロファイルの影響を受けるということです。

以下の画像は極端な例で、mix-blend-mode: multiply#ff0000#00ffff を指定したとき、計算上は #000000 になりますが、MacBookでの表示は赤茶色になります。

(追記2022/12/02 13:20: 記事公開時、#ff0000#0000ff と書いていましたが、正しくは #ff0000#00ffff でした)

multiplyと#ff0000と#0000ffを指定した。計算結果は#000000(真っ黒)だが、CSSによるレンダリング結果は赤茶色になっている

この状況はディスプレイのカラープロファイルをsRGBにすると解消しますが、つまりカラープロファイルの違いによって望みどおりの色にならないという点に注意が必要そうです。

mix-blend-mode によって作られる色

ツールと、MDNのblend-modeの解説を読んで、なんとなく各modeにどんな効果があるのかわかってきました。

ツールでの実装では、入力された上の色 (source) と下の色 (backdrop) の16進数のカラーコードをR,G,Bにバラし、255で割って0〜1の値にして、そこに mix-blend-mode ごとの処理をして、255を掛けてカラーコードに戻すようなことをやっています。 HSL色空間での計算を伴う hue saturation color luminosity以外では R,G,Bそれぞれのチャネルの数値に対する処理をします。

// 下の色(0〜1の数字の配列にする)
const b = "#e9e7e7".match( /^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i).slice(1).map((e) => parseInt(e, 16) / 255);
// 上の色(0〜1の数字の配列にする)
const s = "#2864f0".match( /^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i).slice(1).map((e) => parseInt(e, 16) / 255);

// R,G,Bの各チャンネルにmix-blend-modeの処理をする ( blend(b:number, s:number) => number な関数を用意しておく)
const colorArray = [...Array(3)].map((_, i) => blend(b[i], s[i]));
// 配列をカラーコードに戻す
const colorCode = colorArray.reduce((p, c) => `${p}${`00${Math.round(c * 255).toString(16)}`.slice(-2)}`, "#");

normal

デフォルトのモードで、単純に上の色がそのまま使われます。

const normal = (b, s) => s;

normalの例

multiplyscreen

multiply では色同士が乗算され、1以下の数字同士の乗算となるので暗い色になります。screen はそれぞれの色を反転させて(つまり1から引いて)乗算し、それをさらに反転した色になります。明るい色になります。

という説明だとわかりにくいですが、multiply は「色の三原色」の重なり方、screenは「光の三原色」の重なり方だと理解するのがよさそうです。

const multiply = (b, s) => b * s;
const screen = (b, s) => 1 - (1 - b) * (1 - s);

multiplyとscreenの例

darkenlighten

darken は R,G,Bをそれぞれ比較して暗いほう(小さいほう)の数字が採用されます。これも暗くなります。lighten は明るいほう(大きいほうの数字が採用され、明るくなります。

R,G, Bそれぞれがバラバラに上の色や下の色から取った数字が採用されるので、出来上がりの色が予測しづらいかもしれません。

const darken = (b, s) => Math.min(b, s);
const lighten = (b, s) => Math.max(b, s);

darkenとlightenの例

hard-lightoverlaysoft-light

hard-light は上の色が暗ければ multiply 明るければ screen の効果になりますが、上の色を2倍したり1を引いたりするので、multiplyscreen より効果が弱くなっています。

overlayhard-light の上の色と下の色を入れ替えたものです。

soft-light は計算式がややこしいですが、hard-light よりさらに弱い効果になるようです。

const hardLight = (b, s) =>
  s <= 0.5 ? multiply(b, s * 2) : screen(b, 2 * s - 1);const overlay = (b, s) => hardLight(s, b);
const overlay = (b, s) => hardLight(s, b);
const softLight = (b, s) =>
  s < 0.5
    ? b - (1 - 2 * s) * b * (1 - b)
    : b +
      (2 * s - 1) * (b < 0.25 ? ((16 * b - 12) * b + 4) * b : Math.sqrt(b) - b);

hard-lightとoverlayとsoft-lightの例

color-dodgecolor-burn

color-dodgescreen に、color-burnmultiply に似た効果になります。multiplyscreenよりも強い効果が得られるようです。

const colorDodge = (b, s) =>
  b === 0 ? 0 : s === 1 ? 1 : Math.min(1, b / (1 - s));
const colorBurn = (b, s) =>
  b === 1 ? 1 : s === 0 ? 0 : 1 - Math.min(1, (1 - b) / s);

color-dodgeとcolor-burnの例

differenceexclusion

difference はR,G,Bそれぞれの差の絶対値、exclusiondifference よりも変化の小さな効果があります。

const difference = (b, s) => Math.abs(b - s);
const exclusion = (b, s) => b + s - 2 * b * s;

differenceとexclusionの例

huesaturationcolorluminosity

ここまでのblend-modeはRGBに対する計算でしたが、 huesaturationcolorluminosityの4つは、HSLに対する計算になります。カラーフィルタみたいな使い方ができそう。

  • hue は上の色の色相(H)と、下の色の彩度(S)と明度(L)を持つ色になります。
  • saturation は上の色の彩度(L)と、下の色の色相(H)と明度(L)を持つ色になります。
  • color は上の色の色相(H)と彩度(S)と、下の色の明度(L)を持つ色になります。
  • luminosity は上の色の明度(L)と、下の色の色相(H)と彩度(S)を持つ色になります。

HSLを扱う部分が複雑なのでコード例は省略します。

hueとsaturationとcolorとluminosityの例

まとめ

なるほどわからん。

明日は Takeru Ichii さんです。お楽しみに。