CSSで画像を2色のデュオトーンにする方法

​​2026年11月14日​CSS​HTML,SVG

デュオトーンは、画像の暗い部分を1つの色、明るい部分をもう1つの色にして、その間の明るさを2色のあいだの色でつなぐ表現です。CSS では、SVG のフィルターを filter: url() で当てる方法と、mix-blend-mode で色を重ねる方法の2つで作れます。指定した2色のとおりに仕上がるのは SVG のフィルターのほうで、mix-blend-mode では明るい側の色が少しずれました。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。上に色のある景色、下に黒から白への階調を置いた 256×192 ピクセルの画像を作り、2色にしたものを Playwright で撮って、画素の色を数値で調べました。Firefox と Safari、スマートフォンの実機は試していません。ほかのブラウザーの対応状況は、MDN のブラウザー互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

ここでは、暗い部分を #1f2a30(黒に近い紺)、明るい部分を #00c8e8(水色)にします。

上から3枚の画像。1枚目の「元の画像」は、青から橙の空、黄色の太陽、緑の丘、赤い四角の家と、下に黒から白への帯。2枚目の「SVG のフィルター」と3枚目の「CSS の重ね合わせ」は、同じ絵が紺から水色の2色だけで描かれ、下の帯も紺から水色に変わっている。3枚目のほうが少し明るく、灰色がかっている
元の画像と、2つの方法で2色にした結果。下の帯は、元の画像では黒から白への階調

SVG のフィルターで、指定した2色にする

HTML の中に、見えない <svg> を置いてフィルターを定義し、CSS の filter で画像に当てます。

html
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
  <filter id="duotone" color-interpolation-filters="sRGB">
    <!-- まず白黒にする -->
    <feColorMatrix type="saturate" values="0" />
    <!-- 黒を #1f2a30、白を #00c8e8 に置きかえ、間をつなぐ -->
    <feComponentTransfer>
      <feFuncR type="table" tableValues="0.122 0" />
      <feFuncG type="table" tableValues="0.165 0.784" />
      <feFuncB type="table" tableValues="0.188 0.910" />
    </feComponentTransfer>
  </filter>
</svg>

<img class="duotone-svg" src="photo.png" width="256" height="192" alt="">
css
.duotone-svg { filter: url(#duotone); }

フィルターは2段階で色を変えます。

1段目の <feColorMatrix> は、type="saturate" で values="0" にすると、色の鮮やかさを0にして白黒にします。Filter Effects の仕様では、このときの明るさは赤・緑・青に 0.2126、0.7152、0.0722 を掛けて足した値です。CSS の grayscale(1) も同じ係数です。

2段目の <feComponentTransfer> は、赤・緑・青のそれぞれを、子の要素 <feFuncR>・<feFuncG>・<feFuncB> で決めた値に置きかえます。type="table" の tableValues に2つの数を書くと、0(黒)を1つめの数に、1(白)を2つめの数にして、その間を直線でつなぎます。白黒にした後なので、どの画素も「暗い色から明るい色までの線の上のどこか」の色になります。

tableValues には、色の値を 0〜1 で書きます。16進数の色を 255 で割った値です。

赤 緑 青
暗い部分 #1f2a30 31 ÷ 255 = 0.122 42 ÷ 255 = 0.165 48 ÷ 255 = 0.188
明るい部分 #00c8e8 0 ÷ 255 = 0 200 ÷ 255 = 0.784 232 ÷ 255 = 0.910

検証環境で、下の階調の帯を測ったところ、黒だった左端は (31, 42, 47)、白だった右端は (0, 199, 232) になりました。#1f2a30 は (31, 42, 48)、#00c8e8 は (0, 200, 232) なので、どちらも1以内の差です。景色の部分も含めて全部の画素を、「白黒にした明るさで2色の間をつないだ色」と比べると、差はいちばん大きいところで 1.33 でした。

検証環境の Chromium では、<svg> を style="display: none" で隠しても、フィルターは同じ色で効きました。

color-interpolation-filters="sRGB" を忘れると色がずれる

上のコードの <filter> には、color-interpolation-filters="sRGB" を書いています。これを書かないと、フィルターは指定した色になりません。

color-interpolation-filters は、フィルターがどの色空間で計算するかを決める属性で、初期値は linearRGB です。linearRGB では、明るさを光の量に比例する値に直してから計算します。tableValues の数もその値として扱われるので、#1f2a30 を 255 で割った値を書いても、画面に出る色は #1f2a30 になりません。CSS の grayscale() のようなフィルター関数は、この属性に関係なく sRGB で計算すると、仕様に書かれています。

上下2枚の画像。上の「color-interpolation-filters="sRGB" あり」は、紺から水色の2色の絵で、下の帯の左端が黒に近い紺。下の「指定なし(初期値の linearRGB)」は、全体が白っぽい灰色がかった水色で、下の帯の左端も灰色になっている
同じ tableValues のフィルターで、color-interpolation-filters の指定だけを変えた

検証環境で、color-interpolation-filters だけを消したフィルターを当てると、階調の帯の色は次のようになりました。

帯の位置 sRGB を指定 指定なし(linearRGB)
左端(元は黒) (31, 42, 47) (98, 113, 119)
まん中(元は 128 の灰色) (15, 121, 140) (86, 148, 158)
右端(元は白) (0, 199, 232) (0, 229, 245)

指定しないと、黒が灰色になり、画像全体が白っぽくなりました。デュオトーンのフィルターでは、color-interpolation-filters="sRGB" を <filter> に書いておきます。

mix-blend-mode で、CSS だけで2色にする

HTML に <svg> を置かずに、CSS だけで作る方法もあります。画像を囲む要素の背景を明るい色にし、その上に白黒にした画像と、暗い色で塗った層(::after)を、mix-blend-mode で順に重ねます。

html
<div class="duotone-blend">
  <img src="photo.png" width="256" height="192" alt="">
</div>
css
.duotone-blend {
  position: relative;
  background: #00c8e8; /* 明るい部分の色 */
}
.duotone-blend img {
  filter: grayscale(1);
  mix-blend-mode: multiply;
}
.duotone-blend::after {
  content: "";
  position: absolute;
  inset: 0;
  background: #1f2a30; /* 暗い部分の色 */
  mix-blend-mode: screen;
}

Compositing and Blending の仕様では、色の値を 0〜1 としたとき、multiply は下の色と上の色を掛けた値、screen は「下 + 上 − 下 × 上」です。白黒にした画像を水色の背景に multiply で重ねると、黒は黒のまま、白は水色になります。その上に紺の層を screen で重ねると、黒だったところは紺になります。

検証環境で測ると、階調の帯の左端(元は黒)は (31, 42, 48) で、#1f2a30 と同じでした。ところが右端(元は白)は (31, 209, 236) で、#00c8e8 の (0, 200, 232) より明るくなりました。紺の層は明るい部分にも screen で重なるので、白だったところは、水色と紺を screen で重ねた色になります。この例では明るい色の赤が 0 で、紺の赤は 31 なので、階調の帯で測った5か所とも、赤は 31 でした。全部の画素を、multiply と screen の式で計算した色と比べると、差はいちばん大きいところで 1.21 で、式どおりに重なっていました。

暗い側の色は指定どおりになりますが、明るい側の色は、2色を screen で重ねた色になります。明るい色を指定どおりに出したいときは、SVG のフィルターを使います。

どちらを使うか

2つの方法の違いをまとめます。

SVG のフィルター mix-blend-mode
黒だったところの色 指定した暗い色 指定した暗い色
白だったところの色 指定した明るい色 2色を screen で重ねた色
書く場所 HTML に <svg>、CSS に filter CSS だけ(HTML には画像を囲む要素を書く)
色の書き方 0〜1 の数に直して書く CSS の色のまま書ける

MDN のブラウザー互換性データでは、CSS の filter プロパティ自体は Chrome 53、Edge 12、Firefox 35、Safari 9.1 から、mix-blend-mode は Chrome 41、Edge 79、Firefox 32、Safari 8 から対応しています。<feColorMatrix> と <feComponentTransfer> は、Chrome 5、Edge 12、Firefox 3、Safari 6 から対応しています。

​Posted by 86