はじめに
この記事では、Adobe Photoshop などのデザインツールでおなじみのブレンドモードについて扱います。
コードと実行結果は SwiftUI のものですが、ブレンドモードは Core Graphics が提供しているので、SwiftUI を使わない開発でも知識としては使えると思います。
さて、本題です。
SwiftUI で2つのビューをブレンドしてみます。こんな感じのイメージです。
緑が透過なビューの場合は単純に上に載せるだけなので、非透過な緑色のビューとの合成を考えます。
この場合のサンプルコードはこちら。
struct ContentView: View {
var body: some View {
ZStack {
Image("lenna")
.resizable()
.scaledToFit()
Rectangle()
.fill(Color.green)
.blendMode(.multiply) // ← ココ!
}
.edgesIgnoringSafeArea(.all)
}
}
簡単な説明
-
ZStack
でImage
の上にRectangle
を載せる-
Image
を配置- アスペクト比を維持してビューにフィット
-
Rectangle
を配置-
green
で塗りつぶし - 乗算ブレンド
-
-
- セーフエリアを無視して配置
実行結果
BlendMode
を指定して配置するだけなので、とても簡単ですが、次のことが気になりました。
-
乗算ブレンド
.multiply
とはどのような合成方法なのか? - 他の
BlendMode
を指定した場合はどのような結果が得られるのか?
ということで、本記事では各種ブレンドモードについての情報と実際の表示結果を載せました。
表示結果は SwiftUI のプレビュー機能を使用しています。
環境
- Xcode Version 11.5 (11E608c)
BlendMode
SwiftUI の API リファレンスには概要が記載されていなかったので、Core Graphics の API リファレンスも参照しました。次のように記載されていました。
You can find more information on blend modes, including examples of images produced using them, and many mathematical descriptions of the modes, in PDF Reference, Fourth Edition, Version 1.5, Adobe Systems, Inc. If you are a former QuickDraw developer, it may be helpful for you to think of blend modes as an alternative to transfer modes1
Adobe Systems の PDF Reference, Fourth Edition に正確な情報が記載されているとのことです。
この資料には、全てではないですが、各ブレンドモードの数式も記載がありました。
PDF Reference に記載のあるブレンドモード
BlendMode | CGBlendMode | 名称 |
---|---|---|
normal | kCGBlendModeNormal | 通常 |
multiply | kCGBlendModeMultiply | 乗算 |
screen | kCGBlendModeScreen | スクリーン |
overlay | kCGBlendModeOverlay | オーバーレイ |
darken | kCGBlendModeDarken | 比較(暗) |
lighten | kCGBlendModeLighten | 比較(明) |
colorDodge | kCGBlendModeColorDodge | 覆い焼きカラー |
colorBurn | kCGBlendModeColorBurn | 焼き込みカラー |
softLight | kCGBlendModeSoftLight | ソフトライト |
hardLight | kCGBlendModeHardLight | ハードライト |
difference | kCGBlendModeDifference | 差の絶対値 |
exclusion | kCGBlendModeExclusion | 除外 |
hue | kCGBlendModeHue | 色相 |
saturation | kCGBlendModeSaturation | 彩度 |
color | kCGBlendModeColor | カラー |
luminosity | kCGBlendModeLuminosity | 輝度 |
以下、数学的な説明が必要な場合は、PDF Reference, Fourth Edition で記載されていた次の変数を使います。
変数 | 意味 |
---|---|
$C_b$ | 背景の色(基本色) |
$C_s$ | 前面の色(合成色) |
$C(C_b, C_s)$ | ブレンド関数 |
上記変数は各色成分をもつベクトルです。また、今回扱う関数は、各色成分ごとに分離可能な関数です。
normal
通常
各ピクセルを編集またはペイントして結果色を作成します。これは、初期設定のモードです(通常モードは、モノクロ 2 階調画像やインデックスカラー画像で作業するときには、2 階調化と呼ばれます)。2
ブレンド関数は次の通りです。前面をそのまま反映します。
B(c_b,c_s) = c_s
multiply
乗算
各チャンネル内のカラー情報に基づき、基本色と合成色を乗算します。結果色は暗いカラーになります。どのカラーも、ブラックで乗算すると結果はブラックになります。どのカラーも、ホワイトで乗算した場合は変更されません。ブラックまたはホワイト以外のカラーでペイントしている場合、ペイントツールで繰り返しストロークを描くとカラーは徐々に暗くなります。この効果は、複数のマーカーペンで描画したような効果が得られます。3
ブレンド関数は次の通りです。背景カラーと前面カラーの RGB の構成要素をそれぞれ乗算します。
B(c_b,c_s) = c_b × c_s
背景ビューと前面ビューを入替えても同じ結果を得ることができます。
色の各成分は、0 以上 1 以下の小数です。特に ブラック(0, 0, 0), ホワイト(1, 1, 1) なので、
ブラックで乗算すると結果はブラック、ホワイトで乗算した場合は変更されないことがわかります。
また、0 以上 1 未満の数は乗算を繰り返すほど、0 に近づくので、
multiply ブレンドモードを繰り返すほど徐々に暗くなります。
screen
スクリーン
各チャンネル内のカラー情報に基づき、合成色と基本色を反転したカラーを乗算します。結果色は明るいカラーになります。ブラックでスクリーニングすると、カラーは変更されません。ホワイトでスクリーニングすると、ホワイトになります。この効果は、複数の写真スライドを重ね合わせて投影したような効果が得られます。4
ブレンド関数は次の通りです。
B(c_b,c_s) = 1 – (1–c_b)×(1–c_s)
背景カラーと前面カラーをそれぞれ反転させたものを乗算して、その結果の値を反転させます。
この式から、multiply と逆の性質を得ることができます。
- ブラックとスクリーンをすると変更されない
- ホワイトとスクリーンをすると結果はホワイト
- 繰り返すほど徐々に明るくなる
overlay
オーバーレイ
基本色に応じて、カラーを乗算またはスクリーンします。基本色のハイライトおよびシャドウを保持しながら、パターンまたはカラーを既存のピクセルに重ねます。基本色は、置き換えられませんが、合成色と混合されて基本色の明るさまたは暗さを反映します。5
darken
比較(暗)
各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか暗い方を結果色として選択します。合成色よりも明るいピクセルが置き換えられ、合成色よりも暗いピクセルは変更されません。6
ブレンド関数は次の通りです。
B(c_b, c_s) = min(c_b, c_s)
lighten
比較(明)
各チャンネル内のカラー情報に基づき、基本色または合成色のいずれか明るい方を結果色として選択します。合成色よりも暗いピクセルが置き換えられ、合成色よりも明るいピクセルは変更されません。7
ブレンド関数は次の通りです。
B(c_b, c_s) = max(c_b, c_s)
colorDodge
覆い焼きカラー
各チャンネルのカラー情報に基づき、基本色を明るくして基本色と合成色のコントラストを落とし、合成色を反映します。ブラックと合成しても変化はありません。8
colorBurn
焼き込みカラー
各チャンネルのカラー情報に基づき、基本色を暗くして基本色と合成色のコントラストを強くし、合成色を反映します。ホワイトで合成した場合は、何も変更されません。9
softLight
ソフトライト
合成色に応じて、カラーを暗くまたは明るくします。画像上でスポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像は覆い焼きされたかのように明るくなります。合成色が 50 %グレーよりも暗い場合、画像は焼き込んだように暗くなります。純粋な黒または白でペイントすると、その部分の明暗ははっきりしますが、純粋な黒または白にはなりません。10
hardLight
ハードライト
合成色に応じて、カラーを乗算またはスクリーンします。画像上で直接スポットライトを照らしたような効果が得られます。合成色(光源)が 50 %グレーよりも明るい場合、画像はスクリーンされたかのように明るくなります。これは、画像にハイライトを追加するときに役立ちます。合成色が 50 %グレーよりも暗い場合、画像は乗算されたかのように暗くなります。これは、画像にシャドウを追加するときに役立ちます。純粋なホワイトまたはブラックでペイントすると、純粋なホワイトまたはブラックになります。11
difference
差の絶対値
各チャンネル内のカラー情報に基づいて、合成色を基本色から取り除くか、基本色を合成色から取り除きます。明るさの値の大きい方のカラーから小さい方のカラーを取り除きます。ホワイトと合成すると基本色の値が反転しますが、ブラックと合成しても変化はありません。12
ブレンド関数は次の通りです。
B(c_b, c_s) = | c_b – c_s |
ホワイトとブレンドすると色反転ができます。
exclusion
除外
差の絶対値モードと似ていますが、効果のコントラストはより低くなります。ホワイトと合成すると、基本色の値が反転しますが、ブラックと合成しても変化はありません。13
hue
色相
ベースカラーの輝度と彩度、およびブレンドカラーの色相を持つ最終カラーが作成されます。14
saturation
彩度
基本色の輝度と色相および合成色の彩度を使用して、結果色を作成します。このモードで彩度ゼロ(グレー)の領域をペイントした場合は、何も変更されません。15
color
カラー
基本色の輝度と、合成色の色相および彩度を使用して、結果色を作成します。これにより、画像内のグレーレベルが保持され、モノクロ画像のカラー化およびカラー画像の階調化に役立ちます。16
luminosity
輝度
基本色の色相および彩度と、合成色の輝度を使用して、結果色を作成します。このモードでは、カラーモードの反対の効果が作成されます。17
サンプルコード
今回のスクリーンショットで使ったコードはこちら。
表示データ
import SwiftUI
struct Item: Hashable {
let mode: BlendMode, name: String
static var items: [Item] = [
Item(mode: .normal, name: ".normal"),
Item(mode: .multiply, name: ".multiply"),
Item(mode: .screen, name: ".screen"),
Item(mode: .overlay, name: ".overlay"),
Item(mode: .darken, name: ".darken"),
Item(mode: .lighten, name: ".lighten"),
Item(mode: .colorDodge, name: ".colorDodge"),
Item(mode: .colorBurn, name: ".colorBurn"),
Item(mode: .softLight, name: ".softLight"),
Item(mode: .hardLight, name: ".hardLight"),
Item(mode: .difference, name: ".difference"),
Item(mode: .exclusion, name: ".exclusion"),
]
}
画像と色のブレンド
import SwiftUI
struct ImageBlendView: View {
var color: Color
var mode: BlendMode
var body: some View {
ZStack {
Image("lenna").resizable().scaledToFit()
Rectangle().fill(color).blendMode(mode)
}
}
}
struct ImageBlendViewSamples: View {
var mode: BlendMode
var colors: [Color] = [.clear, .red, .green, .blue, .black, .white]
var body: some View {
HStack(spacing: 0) { [colors, mode] in
ForEach(0..<colors.count) { i in
ImageBlendView(color: colors[i], mode: mode)
}
}
}
}
// MARK: - PreviewProvider
struct ImageBlendViewSamples_Previews: PreviewProvider {
static var previews: some View {
Group { [items = Item.items] in
ForEach(0..<items.count) { i in
ImageBlendViewSamples(mode: items[i].mode)
.previewDisplayName(items[i].name)
}
}
.previewLayout(.fixed(width: 100 * 6, height: 100))
}
}
円のブレンド
import SwiftUI
struct ColorBlendView: View {
var mode: BlendMode
var background: Color
var colors: [Color]
var body: some View {
GeometryReader<AnyView> { [mode, colors] geometry in
let edge = min(geometry.size.width, geometry.size.height)/2
let offset = edge/3
let arg: (Int) -> CGFloat = { 2*(.pi)*CGFloat($0)/CGFloat(colors.count) - (.pi)/2 }
return AnyView(
ZStack {
ForEach(0..<colors.count) { index in
Circle()
.fill(colors[index])
.frame(width: edge)
.offset(x: offset*cos(arg(index)), y: offset*sin(arg(index)))
.blendMode(mode)
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
)
}
.background(background)
.edgesIgnoringSafeArea(.all)
}
}
struct ColorBlendViewSamples: View {
var mode: BlendMode
var foregrounds: [[Color]] = [
[.red, .green, .blue],
.init(repeating: .green, count: 7)
]
var backgrounds: [Color] = [
Color(red: 0, green: 0, blue: 0),
Color(red: 1, green: 1, blue: 1),
]
var body: some View {
HStack(spacing: .zero) { [mode, foregrounds, backgrounds] in
ForEach(0..<backgrounds.count) { i in
ForEach(0..<foregrounds.count) { j in
ColorBlendView(mode: mode, background: backgrounds[i], colors: foregrounds[j])
}
}
}
}
}
// MARK: - PreviewProvider
struct ColorBlendViewSamples_Previews: PreviewProvider {
static var previews: some View {
Group { [items = Item.items] in
ForEach(0..<items.count) { i in
ColorBlendViewSamples(mode: items[i].mode)
.previewDisplayName(items[i].name)
}
}
.previewLayout(.fixed(width: 120 * 4, height: 120))
}
}
参考
- CGBlendMode | Apple Developer Documentation
- BlendMode | Apple Developer Documentation
- Adobe PDF Reference Archives
- Adobe Photoshop での描画モード
- 標準画像データベース[神奈川工大 信号処理応用研究室]