次の方法で共有


Porter-Duff ブレンド モード

Porter-Duff ブレンドモードは、Lucasfilm で働いていたときに合成の代数を開発した Thomas Porter と Tom Duff の名前にちなんでいます。 彼らの論文「Compositing Digital Images」(デジタル画像の合成) は、『Computer Graphics』1984 年 7 月号 (253 から 259 ページ) で発表されました。 ブレンド モードは、さまざまな画像を組み合わせて 1 つの複合シーンにする合成には、なくてはならないものです。

Porter-Duff サンプル

Porter-Duff の概念

茶色い四角形が、表示サーフェスの左側と上部の 3 分の 2 を占めているとします。

Porter-Duff ターゲット

この領域は、"ターゲット" と呼ばれることもあれば、"背景" や "バックグラウンド" と呼ばれることもあります。

ターゲットと同じサイズの、次のような四角形を描画しようと思います。 四角形は、右側と下部の 3 分の 2 を占める青い領域以外は透明です。

Porter-Duff ソース

これは、"ソース" と呼ばれますが、"前景" と呼ばれることもあります。

ターゲットにソースを表示すると、このような表示になると予想されます。

Porter-Duff ソースが上

ソースの透明なピクセルを使用すると、背景が透明になり、ソースの青いピクセルで背景が隠れます。 これは通常のケースであり、SkiaSharp では SKBlendMode.SrcOver と呼ばれます。 この値は、SKPaint オブジェクトが最初にインスタンス化されるときの BlendMode プロパティの既定の設定です。

ただし、別の効果のために別のブレンド モードを指定することもできます。 SKBlendMode.DstOver を指定すると、ソースとターゲットが交差する領域に、ソースではなくターゲットが表示されます。

Porter-Duff ターゲットが上

SKBlendMode.DstIn ブレンド モードでは、ソースとターゲットが交差する領域のみがターゲットの色を使用して表示されます。

Porter-Duff ターゲットが中

SKBlendMode.Xor (排他的論理知) のブレンド モードでは、2 つの領域が重なる場所に何も表示されません。

Porter-Duff 排他的 OR

色が付けられたターゲットの四角形とソースの四角形によって、表示サーフェスが 4 つの固有の領域に効果的に分割されます。これらの領域は、ターゲットの四角形とソースの四角形が存在しているかどうかに応じて、さまざまな方法で色が付けられます。

Porter-Duff

コピー先とソースの両方がそれらの領域で透明であるため、右上と左下の四角形は常に空白になります。 コピー先の色は左上の領域を占めるので、その領域はコピー先の色で色分けするか、まったく色分けできません。 同様に、ソースの色は右下の領域を占めるので、その領域はソースの色で色分けすることも、まったく色分けすることもできません。 中央のターゲットとソースの交差部分は、ターゲットまたはソースの色で色を付けるか、色をまったく付けないでおくことができます。

組み合わせの合計数は、2 (左上) 掛ける 2 (右下) 掛ける 3 (中央) または 12 です。 これらは、基本的な 12 の Porter-Duff 合成モードです。

「"デジタル画像の合成"」の終盤で (256 ページ)、Porter と Duff は、plus という 13 番目のモードを付け加えています (SkiaSharp SKBlendMode.Plus メンバーと W3C Lighter モードに対応しますが、W3C Lighten とは別のものです)。この Plus モードを使用すると、ターゲットの色とソースの色が追加されます。このプロセスについては、後ほど詳しく説明します。

Skia では、Modulate という 14 番目のモードが追加されています。これは、ターゲットの色とソースの色が乗算される点を除き、Plus によく似ています。 これは、追加の Porter-Duff ブレンド モードとして扱うことができます。

SkiaSharp で定義されている 14 の Porter-Duff モードを次に示します。 次の表は、上図の 3 つの空白以外の各領域を色分けする方法を示しています。

モード 宛先 インターセクション ソース
Clear
Src ソース x
Dst x 宛先
SrcOver x ソース x
DstOver x 宛先 x
SrcIn ソース
DstIn Destination (公開先)
SrcOut x
DstOut X
SrcATop x ソース
DstATop Destination (公開先) x
Xor X X
Plus x SUM x
Modulate Product

これらのブレンド モードは対称です。 ソースとターゲットを入れ替えることができ、すべてのモードは引き続き使用できます。

モードの名前付け規則は、いくつかの簡単な規則に従います。

  • SrcDst は、その名前のとおり、ソースまたはターゲットのピクセルのみを表示できることを意味します。
  • Over サフィックスは、交差部分に表示される内容を示します。 ソースまたはターゲットは、もう一方の上に描画されます。
  • In サフィックスは、交差部分のみが色付けされていることを意味します。 出力は、もう一方の "中" に表示されているソースまたはターゲットの一部のみに制限されます。
  • Out サフィックスは、交差部分に色が付けられていないことを意味します。 出力は、交差部分の "外" にあるソースまたはターゲットの一部のみです。
  • ATop サフィックスは、InOut の和集合です。これには、ソースまたはターゲットがもう一方の "上" にある領域が含まれます。

Plus モードと Modulate モードの違いに注意してください。 これらのモードでは、ソースとターゲットのピクセルで異なる種類の計算が実行されます。 詳細については、後ほど詳しく説明します。

[Porter-Duff Grid] (Porter-Duff グリッド) ページには、1 つの画面にグリッド形式で 14 のモードがすべて表示されます。 各モードは、SKCanvasView の個別のインスタンスです。 そのため、クラスは、PorterDuffCanvasView という名前の SKCanvasView から派生します。 静的コンストラクターを使用すると、同じサイズの 2 つのビットマップが作成されます。1 つには左上の領域に茶色い四角形、もう 1 つには青い四角形が含まれます。

class PorterDuffCanvasView : SKCanvasView
{
    static SKBitmap srcBitmap, dstBitmap;

    static PorterDuffCanvasView()
    {
        dstBitmap = new SKBitmap(300, 300);
        srcBitmap = new SKBitmap(300, 300);

        using (SKPaint paint = new SKPaint())
        {
            using (SKCanvas canvas = new SKCanvas(dstBitmap))
            {
                canvas.Clear();
                paint.Color = new SKColor(0xC0, 0x80, 0x00);
                canvas.DrawRect(new SKRect(0, 0, 200, 200), paint);
            }
            using (SKCanvas canvas = new SKCanvas(srcBitmap))
            {
                canvas.Clear();
                paint.Color = new SKColor(0x00, 0x80, 0xC0);
                canvas.DrawRect(new SKRect(100, 100, 300, 300), paint);
            }
        }
    }
    ···
}

インスタンス コンストラクターには、SKBlendMode 型のパラメータがあります。 このパラメータはフィールドに保存されます。

class PorterDuffCanvasView : SKCanvasView
{
    ···
    SKBlendMode blendMode;

    public PorterDuffCanvasView(SKBlendMode blendMode)
    {
        this.blendMode = blendMode;
    }

    protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Find largest square that fits
        float rectSize = Math.Min(info.Width, info.Height);
        float x = (info.Width - rectSize) / 2;
        float y = (info.Height - rectSize) / 2;
        SKRect rect = new SKRect(x, y, x + rectSize, y + rectSize);

        // Draw destination bitmap
        canvas.DrawBitmap(dstBitmap, rect);

        // Draw source bitmap
        using (SKPaint paint = new SKPaint())
        {
            paint.BlendMode = blendMode;
            canvas.DrawBitmap(srcBitmap, rect, paint);
        }

        // Draw outline
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 2;
            rect.Inflate(-1, -1);
            canvas.DrawRect(rect, paint);
        }
    }
}

OnPaintSurface オーバーライドを実行すると、2 つのビットマップが描画されます。 1 つ目は通常どおり描画されます。

canvas.DrawBitmap(dstBitmap, rect);

2 つ目は、BlendMode プロパティがコンストラクター引数に設定されている SKPaint オブジェクトで描画されます。

using (SKPaint paint = new SKPaint())
{
    paint.BlendMode = blendMode;
    canvas.DrawBitmap(srcBitmap, rect, paint);
}

OnPaintSurface オーバーライドの残りの処理では、サイズを示すために、ビットマップの周囲に四角形が描画されます。

PorterDuffGridPage クラスを使用すると、blendModes 配列の各メンバーに 1 個ずつ、PorterDurffCanvasView のインスタンスが 14 個作成されます。 配列内の SKBlendModes メンバーの順序は、類似するモードどうしを近くに配置するため、表とは少し異なります。 PorterDuffCanvasView の 14 個のインスタンスは、Grid のラベルと併せて整理されます。

public class PorterDuffGridPage : ContentPage
{
    public PorterDuffGridPage()
    {
        Title = "Porter-Duff Grid";

        SKBlendMode[] blendModes =
        {
            SKBlendMode.Src, SKBlendMode.Dst, SKBlendMode.SrcOver, SKBlendMode.DstOver,
            SKBlendMode.SrcIn, SKBlendMode.DstIn, SKBlendMode.SrcOut, SKBlendMode.DstOut,
            SKBlendMode.SrcATop, SKBlendMode.DstATop, SKBlendMode.Xor, SKBlendMode.Plus,
            SKBlendMode.Modulate, SKBlendMode.Clear
        };

        Grid grid = new Grid
        {
            Margin = new Thickness(5)
        };

        for (int row = 0; row < 4; row++)
        {
            grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
            grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
        }

        for (int col = 0; col < 3; col++)
        {
            grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
        }

        for (int i = 0; i < blendModes.Length; i++)
        {
            SKBlendMode blendMode = blendModes[i];
            int row = 2 * (i / 4);
            int col = i % 4;

            Label label = new Label
            {
                Text = blendMode.ToString(),
                HorizontalTextAlignment = TextAlignment.Center
            };
            Grid.SetRow(label, row);
            Grid.SetColumn(label, col);
            grid.Children.Add(label);

            PorterDuffCanvasView canvasView = new PorterDuffCanvasView(blendMode);

            Grid.SetRow(canvasView, row + 1);
            Grid.SetColumn(canvasView, col);
            grid.Children.Add(canvasView);
        }

        Content = grid;
    }
}

結果は次のとおりです。

Porter-Duff グリッド

Porter-Duff ブレンド モードが適切に機能するには、透明度が非常に重要であると確信したいところでしょう。 PorterDuffCanvasView クラスには、Canvas.Clear メソッドに対する合計 3 つの呼び出しが含まれています。 これらのすべてで、すべてのピクセルを透明に設定するパラメータなしのメソッドが使用されます。

canvas.Clear();

ピクセルが不透明な白に設定されるように、これらの呼び出しのいずれかを変更してみてください。

canvas.Clear(SKColors.White);

その変更の後、一部のブレンド モードは機能しているように見えますが、機能していないように見えるものもあります。 ソースのビットマップの背景を白に設定すると、ソース ビットのマップにはターゲットを透明に表示できる透明なピクセルがないため、SrcOver モードが機能しません。 ターゲットのビットマップの背景またはキャンバスを白に設定すると、ターゲットには透明のピクセルがないため、DstOver が動作しません。

[Porter-Duff Grid] ページのビットマップを、より単純な DrawRect 呼び出しに置き換えたくなるかもしれません。 これは、ターゲットの四角形では機能しますが、ソースの四角形では機能しません。 ソースの四角形には、青い領域以外も収められている必要があります。 ソースの四角形には、ターゲットの色付きの領域に対応する透明な領域が含められている必要があります。 その場合にのみ、これらのブレンド モードは機能します。

Porter-Duff でマットを使用する

[Brick-Wall Compositing] (レンガの壁の合成) ページには、従来の合成タスクの例が表示されています。写真は、削除が必要な背景のビットマップを含め、複数の部分から組み合わせる必要があります。 こちらは SeatedMonkey.jpg ビットマップです。背景に問題があります。

座った猿

合成の準備として、対応する "マット" が作成されました。これは、別のビットマップであり、画像が表示される部分は黒色、それ以外に部分は透明になっています。 このファイルの名前は SeatedMonkeyMatte.png です。サンプルの Media フォルダー内にあるリソースの 1 つです。

座った猿のマット

これは、うまく作成されたマットでは "ありません"。 マットは、部分的に透明なピクセルが黒いピクセルの全周に含まれているのが理想的ですが、このマットはそうではありません。

[Brick-Wall Compositing] ページの XAML ファイルでは、最終的な画像を合成するプロセスをユーザーに詳しく指示する SKCanvasViewButton がインスタンス化されます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.BrickWallCompositingPage"
             Title="Brick-Wall Compositing">

    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Button Text="Show sitting monkey"
                HorizontalOptions="Center"
                Margin="0, 10"
                Clicked="OnButtonClicked" />

    </StackLayout>
</ContentPage>

分離コード ファイルでは、必要な 2 つのビットマップが読み込まれ、ButtonClicked イベントが処理されます。 Button クリックするたびに、step フィールドがインクリメントされ、Button に新しい Text プロパティが設定されます。 step は、5 に達すると、0 に戻ります。

public partial class BrickWallCompositingPage : ContentPage
{
    SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
        typeof(BrickWallCompositingPage),
        "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");

    SKBitmap matteBitmap = BitmapExtensions.LoadBitmapResource(
        typeof(BrickWallCompositingPage),
        "SkiaSharpFormsDemos.Media.SeatedMonkeyMatte.png");

    int step = 0;

    public BrickWallCompositingPage ()
    {
        InitializeComponent ();
    }

    void OnButtonClicked(object sender, EventArgs args)
    {
        Button btn = (Button)sender;
        step = (step + 1) % 5;

        switch (step)
        {
            case 0: btn.Text = "Show sitting monkey"; break;
            case 1: btn.Text = "Draw matte with DstIn"; break;
            case 2: btn.Text = "Draw sidewalk with DstOver"; break;
            case 3: btn.Text = "Draw brick wall with DstOver"; break;
            case 4: btn.Text = "Reset"; break;
        }

        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        ···
    }
}

プログラムが最初に実行されるときは、Button 以外は何も表示されません。

レンガ壁合成ステップ 0

Button を 1 回押すと、step が 1 にインクリメントされ、PaintSurface ハンドラーに SeatedMonkey.jpg が表示されるようになりました。

public partial class BrickWallCompositingPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        float x = (info.Width - monkeyBitmap.Width) / 2;
        float y = info.Height - monkeyBitmap.Height;

        // Draw monkey bitmap
        if (step >= 1)
        {
            canvas.DrawBitmap(monkeyBitmap, x, y);
        }
        ···
    }
}

SKPaint オブジェクトがないため、ブレンド モードもありません。 ビットマップが画面の下部に表示されます。

レンガ壁合成ステップ 1

Button をもう一度押すと、step が 2 にインクリメントされます。 これは、SeatedMonkeyMatte.png ファイル表示の非常に重要な手順です。

public partial class BrickWallCompositingPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // Draw matte to exclude monkey's surroundings
        if (step >= 2)
        {
            using (SKPaint paint = new SKPaint())
            {
                paint.BlendMode = SKBlendMode.DstIn;
                canvas.DrawBitmap(matteBitmap, x, y, paint);
            }
        }
        ···
    }
}

ブレンド モードは SKBlendMode.DstIn です。つまり、ターゲットは、ソースの透明以外の領域に対応する領域に保持されます。 元のビットマップに対応するターゲットの四角形の残りの部分は透明になります。

レンガ壁合成ステップ 2

背景は削除されています。

次の手順では、サルが座っている歩道に似ている四角形を描画します。 この歩道の外観は、単色シェーダーと Perlin ノイズ シェーダーという 2 つのシェーダーの構成に基づいています。

public partial class BrickWallCompositingPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        const float sidewalkHeight = 80;
        SKRect rect = new SKRect(info.Rect.Left, info.Rect.Bottom - sidewalkHeight,
                                 info.Rect.Right, info.Rect.Bottom);

        // Draw gravel sidewalk for monkey to sit on
        if (step >= 3)
        {
            using (SKPaint paint = new SKPaint())
            {
                paint.Shader = SKShader.CreateCompose(
                                    SKShader.CreateColor(SKColors.SandyBrown),
                                    SKShader.CreatePerlinNoiseTurbulence(0.1f, 0.3f, 1, 9));

                paint.BlendMode = SKBlendMode.DstOver;
                canvas.DrawRect(rect, paint);
            }
        }
        ···
    }
}

この歩道はサルの後ろ側に表示されなくてはならないので、ブレンドモードは DstOver です。 ターゲットは、背景が透明な場所にのみ表示されます。

レンガ壁合成ステップ 3

最後の手順は、レンガの壁の追加です。 プログラムでは、AlgorithmicBrickWallPage クラスの静的プロパティ BrickWallTile として使用できるレンガの壁のビットマップ タイルを使用します。 SKShader.CreateBitmap 呼び出しに平行移動変換が追加され、一番下の行が完全なタイルになるように、タイルがシフトされます。

public partial class BrickWallCompositingPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // Draw bitmap tiled brick wall behind monkey
        if (step >= 4)
        {
            using (SKPaint paint = new SKPaint())
            {
                SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;
                float yAdjust = (info.Height - sidewalkHeight) % bitmap.Height;

                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat,
                                                     SKMatrix.MakeTranslation(0, yAdjust));
                paint.BlendMode = SKBlendMode.DstOver;
                canvas.DrawRect(info.Rect, paint);
            }
        }
    }
}

便宜上、DrawRect 呼び出しではキャンバス全体にこのシェーダーが表示されていますが、DstOver モードでは、出力がキャンバスの透明な領域のみに制限されます。

レンガ壁合成ステップ 4

このシーンを合成する方法は、間違いなく他にもあります。 背景から始めて前景に進んで構築していくことができます。 ただし、ブレンド モードを使用すると、柔軟性が向上します。 特に、マットを使用すると、ビットマップの背景を合成シーンから排除できます。

記事「パスおよび領域でのクリッピング」で学習したように、SKCanvas クラスを使用すると、ClipRect メソッド、ClipPath メソッド、ClipRegion メソッドに対応する 3 種類のクリッピングが定義されます。 Porter-Duff ブレンド モードでは、別の種類のクリッピングが追加されます。これにより、画像を、ビットマップなど、描画可能なものに制限できます。 原則的には、[Brick-Wall Compositing] で使用するマットによって、クリッピング領域が定義されます。

グラデーションの透明度と変化

この記事で前述の Porter-Duff ブレンド モードの例には、関連する画像がすべて含まれており、それらの画像は不透明なピクセルと透明なピクセルで構成されていましたが、部分的に透明なピクセルはありませんでした。 これらのピクセルに対しても、ブレンド モード関数が定義されています。 Skia の「SkBlendMode Reference」(SkBlendMode 参照) に記載の表記が使用されている Porter-Duff ブレンド モードのより正式な定義を次の表に示します。 (SkBlendMode Reference は Skia 参照であるため、C++ 構文を使用します。)

概念的には、各ピクセルの赤、緑、青、アルファの各コンポーネントは、バイトから 0 から 1 の範囲の浮動小数点数に変換されます。 アルファ チャネルの場合、0 は完全に透明、1 は完全に不透明です

次の表の表記では、次の省略形を使用します。

  • Da はターゲットのアルファ チャネルです
  • Dc はターゲットの RGB 色です
  • Sa はソースのアルファ チャネルです
  • Sc はソースの RGB カラーです

RGB カラーには、アルファ値が事前に乗算されています。 たとえば、Sc が純粋な赤色を表しているが Sa は 0x80 の場合、RGB 色は (0x80, 0, 0) となります。 Sa が 0 の場合は、RGB コンポーネントもすべてゼロです。

結果は角かっこの中に示してあり、アルファ チャネルと RGB カラーはコンマで区切ってあります ([アルファ, 色])。 色については、赤、緑、青の各コンポーネントに対して個別に計算が実行されます。

モード 操作
Clear [0, 0]
Src [Sa, Sc]
Dst [Da, Dc]
SrcOver [Sa + Da·(1 – Sa), Sc + Dc·(1 – Sa)
DstOver [Da + Sa·(1 – Da), Dc + Sc·(1 – Da)
SrcIn [Sa·Da, Sc·Da]
DstIn [Da·Sa, Dc·Sa]
SrcOut [Sa·(1 – Da), Sc·(1 – Da)]
DstOut [Da·(1 – Sa), Dc·(1 – Sa)]
SrcATop [Da, Sc·Da + Dc·(1 – Sa)]
DstATop [Sa, Dc·Sa + Sc·(1 – Da)]
Xor [Sa + Da – 2·Sa·Da, Sc·(1 – Da) + Dc·(1 – Sa)]
Plus [Sa + Da, Sc + Dc]
Modulate [Sa·Da, Sc·Dc]

上記の演算は、DaSa が 0 か 1 のいずれかである場合には分析が簡単です。 たとえば、既定の SrcOver モードでは、Sa が 0 であれば Sc も 0 であり、結果は [Da, Dc] というターゲットのアルファと色になります。 Sa が 1 であれば、結果は [Sa, Sc] または [1, Sc] というターゲットのアルファと色になります。

Plus モードと Modulate モードは、ソースとターゲットの組み合わせによって新しい色が出現する可能性がある点で、他のモードとは少し異なります。 Plus モードは、バイト コンポーネントまたは浮動小数点コンポーネントで解釈できます。 前述の [Porter-Duff Grid] ページでは、ターゲットの色が (0xC0、0x80、0x00) で、ソースの色が (0x00、0x80、0xC0) です。 コンポーネントの各ペアが追加されますが、合計は 0xFF のままです。 結果は、(0xC0, 0xFF, 0xC0) という色になります。 これが、交差部分に表示される色です。

Modulate モードでは、RGB 値を浮動小数点数に変換する必要があります。 ターゲットの色は (0.75, 0.5, 0)、ソースは (0, 0.5, 0.75) です。 RGB コンポーネントはそれぞれ乗算され、結果は (0, 0.25, 0) になります。 これが、このモードの [Porter-Duff Grid] ページの交差部分に表示されている色です。

[Porter-Duff Transparency] (Porter-Duff 透明度) ページでは、部分的に透明なグラフィカル オブジェクトに対して Porter-Duff ブレンド モードがどのように動作するかを確認することができます。 XAML ファイルには、Porter-Duff モードが指定された Picker が含まれています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaviews="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.PorterDuffTransparencyPage"
             Title="Porter-Duff Transparency">

    <StackLayout>
        <skiaviews:SKCanvasView x:Name="canvasView"
                                VerticalOptions="FillAndExpand"
                                PaintSurface="OnCanvasViewPaintSurface" />

        <Picker x:Name="blendModePicker"
                Title="Blend Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKBlendMode}">
                    <x:Static Member="skia:SKBlendMode.Clear" />
                    <x:Static Member="skia:SKBlendMode.Src" />
                    <x:Static Member="skia:SKBlendMode.Dst" />
                    <x:Static Member="skia:SKBlendMode.SrcOver" />
                    <x:Static Member="skia:SKBlendMode.DstOver" />
                    <x:Static Member="skia:SKBlendMode.SrcIn" />
                    <x:Static Member="skia:SKBlendMode.DstIn" />
                    <x:Static Member="skia:SKBlendMode.SrcOut" />
                    <x:Static Member="skia:SKBlendMode.DstOut" />
                    <x:Static Member="skia:SKBlendMode.SrcATop" />
                    <x:Static Member="skia:SKBlendMode.DstATop" />
                    <x:Static Member="skia:SKBlendMode.Xor" />
                    <x:Static Member="skia:SKBlendMode.Plus" />
                    <x:Static Member="skia:SKBlendMode.Modulate" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                3
            </Picker.SelectedIndex>
        </Picker>
    </StackLayout>
</ContentPage>

分離コード ファイルでは、線形グラデーションを使用して、同じサイズの 2 つの四角形を塗りつぶします。 ターゲットは、右上から左下へのグラデーションです。 右上隅は茶色ですが、中心に向かって透明にフェードし始めて、左下隅で透明になります。

ソースの四角形は、左上から右下へのグラデーションです。 左上隅は青色ですが、ここでも透明にフェードして、右下隅で透明になります。

public partial class PorterDuffTransparencyPage : ContentPage
{
    public PorterDuffTransparencyPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Make square display rectangle smaller than canvas
        float size = 0.9f * Math.Min(info.Width, info.Height);
        float x = (info.Width - size) / 2;
        float y = (info.Height - size) / 2;
        SKRect rect = new SKRect(x, y, x + size, y + size);

        using (SKPaint paint = new SKPaint())
        {
            // Draw destination
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Right, rect.Top),
                                new SKPoint(rect.Left, rect.Bottom),
                                new SKColor[] { new SKColor(0xC0, 0x80, 0x00),
                                                new SKColor(0xC0, 0x80, 0x00, 0) },
                                new float[] { 0.4f, 0.6f },
                                SKShaderTileMode.Clamp);

            canvas.DrawRect(rect, paint);

            // Draw source
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Left, rect.Top),
                                new SKPoint(rect.Right, rect.Bottom),
                                new SKColor[] { new SKColor(0x00, 0x80, 0xC0),
                                                new SKColor(0x00, 0x80, 0xC0, 0) },
                                new float[] { 0.4f, 0.6f },
                                SKShaderTileMode.Clamp);

            // Get the blend mode from the picker
            paint.BlendMode = blendModePicker.SelectedIndex == -1 ? 0 :
                                    (SKBlendMode)blendModePicker.SelectedItem;

            canvas.DrawRect(rect, paint);

            // Stroke surrounding rectangle
            paint.Shader = null;
            paint.BlendMode = SKBlendMode.SrcOver;
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 3;
            canvas.DrawRect(rect, paint);
        }
    }
}

このプログラムでは、Porter-Duff ブレンド モードを、ビットマップ以外のグラフィック オブジェクトで使用できることを説明します。 ただし、ソースには透明な領域を含める必要があります。 四角形全体がグラデーションになりますが、グラデーションの一部は透明であるため、このようになります。

3 つの例を次に示します。

Porter-Duff の透明度

ターゲットとソースの構成は、Porter-Duff の元の論文「Compositing Digital Images」の 255 ページの図によく似ていますが、このページでは、部分的に透明な領域に対してはブレンド モードが適切に動作することを説明します。

透明なグラデーションは、いくつかの異なる効果に使用できます。 1 つの可能性として、マスクがあります。これは、「SkiaSharp の円形グラデーション」ページの「マスキングの放射状グラデーション」セクションで示している手法に似ています。 [Compositing Mask] (合成マスク) ページの大部分は、前述のプログラムに似ています。 そこでは、ビットマップ リソースを読み込み、表示先の四角形を決定します。 放射状グラデーションは、事前に決定した中心と半径に基づいて作成されます。

public class CompositingMaskPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
        typeof(CompositingMaskPage),
        "SkiaSharpFormsDemos.Media.MountainClimbers.jpg");

    static readonly SKPoint CENTER = new SKPoint(180, 300);
    static readonly float RADIUS = 120;

    public CompositingMaskPage ()
    {
        Title = "Compositing Mask";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Find rectangle to display bitmap
        float scale = Math.Min((float)info.Width / bitmap.Width,
                               (float)info.Height / bitmap.Height);

        SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);

        float x = (info.Width - rect.Width) / 2;
        float y = (info.Height - rect.Height) / 2;
        rect.Offset(x, y);

        // Display bitmap in rectangle
        canvas.DrawBitmap(bitmap, rect);

        // Adjust center and radius for scaled and offset bitmap
        SKPoint center = new SKPoint(scale * CENTER.X + x,
                                        scale * CENTER.Y + y);
        float radius = scale * RADIUS;

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateRadialGradient(
                                center,
                                radius,
                                new SKColor[] { SKColors.Black,
                                                SKColors.Transparent },
                                new float[] { 0.6f, 1 },
                                SKShaderTileMode.Clamp);

            paint.BlendMode = SKBlendMode.DstIn;

            // Display rectangle using that gradient and blend mode
            canvas.DrawRect(rect, paint);
        }

        canvas.DrawColor(SKColors.Pink, SKBlendMode.DstOver);
    }
}

このプログラムとの違いは、グラデーションが中心の黒から始まり、透明になって終わる点です。 これは、DstIn のブレンド モードでビットマップに表示されます。ここでは、ソースの透明ではない領域にのみターゲットが表示されます。

DrawRect 呼び出しの後、放射状グラデーションで定義された円を除き、キャンバスのサーフェス全体が透明になります。 最後の呼び出しが実行されます。

canvas.DrawColor(SKColors.Pink, SKBlendMode.DstOver);

キャンバスの透明の領域全体がピンクになりました。

合成マスク

Porter-Duff モードと部分的に透明なグラデーションを使用すると、画像から画像へと変化させることもできます。 [Gradient Transitions] (グラデーションの変化) ページには、変化の進行レベルを 0 から 1 で示す Slider と、変化の種類を選択する Picker が含まれています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.GradientTransitionsPage"
             Title="Gradient Transitions">

    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="progressSlider"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference progressSlider},
                              Path=Value,
                              StringFormat='Progress = {0:F2}'}"
               HorizontalTextAlignment="Center" />

        <Picker x:Name="transitionPicker"
                Title="Transition"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged" />

    </StackLayout>
</ContentPage>

分離コード ファイルでは、変化を示すために、2 つのビットマップ リソースを読み込みます。 これらは、この記事で前述した [Bitmap Dissolve] (ビットマップのディゾルブ) ページで使用した同じ画像です。 また、このコードを使用すると、線形、放射状、スイープの 3 種類のグラデーションに対応する 3 つのメンバーを持つ列挙型も定義されます。 これらの値は、Picker に読み込まれます。

public partial class GradientTransitionsPage : ContentPage
{
    SKBitmap bitmap1 = BitmapExtensions.LoadBitmapResource(
        typeof(GradientTransitionsPage),
        "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");

    SKBitmap bitmap2 = BitmapExtensions.LoadBitmapResource(
        typeof(GradientTransitionsPage),
        "SkiaSharpFormsDemos.Media.FacePalm.jpg");

    enum TransitionMode
    {
        Linear,
        Radial,
        Sweep
    };

    public GradientTransitionsPage ()
    {
        InitializeComponent ();

        foreach (TransitionMode mode in Enum.GetValues(typeof(TransitionMode)))
        {
            transitionPicker.Items.Add(mode.ToString());
        }

        transitionPicker.SelectedIndex = 0;
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }
    ···
}

分離コード ファイルでは、3 つの SKPaint オブジェクトを作成します。 paint0 オブジェクトには、ブレンド モードを使用しません。 このペイント オブジェクトは、colors 配列で示されているように、黒から透明になるグラデーションの四角形を描画するために使用します。 positions 配列は、Slider の位置に基づいていますが、多少調整されます。 Slider が最小値または最大値である場合、progress 値は 0 または 1 になり、2 つのビットマップのいずれかが完全に表示されます。 positions 配列は、これらの値に応じて設定する必要があります。

progress 値が 0 の場合、positions 配列には値 -0.1 と 0 が含まれます。 SkiaSharp では、その最初の値を 0 に等しくなるように調整します。つまり、グラデーションは 0 でのみ黒、それ以外の場合は透明になります。 progress が 0.5 の場合、配列には値 0.45 と 0.55 が含まれます。 グラデーションは、0 から 0.45 までは黒で、その後透明に変化して、0.55 から 1 で完全に透明になります。 progress が 1 の場合、positions 配列は 1 と 1.1 です。つまり、グラデーションは 0 から 1 で黒になります。

colors 配列と position 配列は両方とも、グラデーションを作成する SKShader の 3 つのメソッドで使用されます。 これらのシェーダーのうち 1 つのみが、Picker の選択内容に基づいて作成されます。

public partial class GradientTransitionsPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Assume both bitmaps are square for display rectangle
        float size = Math.Min(info.Width, info.Height);
        SKRect rect = SKRect.Create(size, size);
        float x = (info.Width - size) / 2;
        float y = (info.Height - size) / 2;
        rect.Offset(x, y);

        using (SKPaint paint0 = new SKPaint())
        using (SKPaint paint1 = new SKPaint())
        using (SKPaint paint2 = new SKPaint())
        {
            SKColor[] colors = new SKColor[] { SKColors.Black,
                                               SKColors.Transparent };

            float progress = (float)progressSlider.Value;

            float[] positions = new float[]{ 1.1f * progress - 0.1f,
                                             1.1f * progress };

            switch ((TransitionMode)transitionPicker.SelectedIndex)
            {
                case TransitionMode.Linear:
                    paint0.Shader = SKShader.CreateLinearGradient(
                                        new SKPoint(rect.Left, 0),
                                        new SKPoint(rect.Right, 0),
                                        colors,
                                        positions,
                                        SKShaderTileMode.Clamp);
                    break;

                case TransitionMode.Radial:
                    paint0.Shader = SKShader.CreateRadialGradient(
                                        new SKPoint(rect.MidX, rect.MidY),
                                        (float)Math.Sqrt(Math.Pow(rect.Width / 2, 2) +
                                                         Math.Pow(rect.Height / 2, 2)),
                                        colors,
                                        positions,
                                        SKShaderTileMode.Clamp);
                    break;

                case TransitionMode.Sweep:
                    paint0.Shader = SKShader.CreateSweepGradient(
                                        new SKPoint(rect.MidX, rect.MidY),
                                        colors,
                                        positions);
                    break;
            }

            canvas.DrawRect(rect, paint0);

            paint1.BlendMode = SKBlendMode.SrcOut;
            canvas.DrawBitmap(bitmap1, rect, paint1);

            paint2.BlendMode = SKBlendMode.DstOver;
            canvas.DrawBitmap(bitmap2, rect, paint2);
        }
    }
}

そのグラデーションは、ブレンド モードを使用せずに四角形で表示されます。 その DrawRect 呼び出しの後、キャンバスには黒から透明へのグラデーションのみが含まれます。 黒の範囲は、Slider 値が高くなるほど大きくなります。

PaintSurface ハンドラーの最後の 4 つのステートメントでは、2 つのビットマップが表示されます。 SrcOut ブレンド モードは、最初のビットマップが背景の透明な領域にのみ表示されることを意味します。 2 番目のビットマップの DstOver モードは、2 番目のビットマップが最初のビットマップが表示されない領域にのみ表示されることを意味します。

次のスクリーンショットは、それぞれ 50% にマークされている 3 つの異なる種類の変化を示しています。

グラデーションの変化