Porter-Duff 混合模式

Porter-Duff 混合模式以 Thomas Porter 和 Tom Duff 的名字命名,他们在 Lucasfilm 工作时开发了合成代数。 他们的论文合成数字图像发表在 1984 年 7 月“计算机图形”期刊的第 253 至 259 页上。 这些混合模式对于合成至关重要,可将各种图像组合成一个合成场景:

Porter-Duff 示例

Porter-Duff 概念

假设一个棕色矩形占据显示表面左侧和顶部的三分之二:

Porter-Duff 目标

该区域称为目标,有时也称为背景

你希望绘制以下矩形,其大小与目标相同。 该矩形是透明的,只不过占据了右侧和底部三分之二的蓝色区域:

Porter-Duff 源

这称为源,有时也称为前景

在目标上显示源时,预期会出现以下情况:

Porter-Duff 源覆盖

源的透明像素允许背景显示出来,而蓝色的源像素则会遮挡背景。 这是正常情况,在 SkiaSharp 中称为 SKBlendMode.SrcOver。 该值是首次实例化 SKPaint 对象时 BlendMode 属性的默认设置。

但是,可以为不同的效果指定不同的混合模式。 如果指定 SKBlendMode.DstOver,则在源和目标相交的区域中将显示目标而不是源:

Porter-Duff 目标覆盖

SKBlendMode.DstIn 混合模式仅使用目标颜色显示目标和源相交的区域:

Porter-Duff 目标交叉

SKBlendMode.Xor 混合模式(异或)导致两个区域重叠的位置不显示任何内容:

Porter-Duff 异或

彩色目标和源矩形有效地将显示表面划分为四个独特区域,可以根据目标和源矩形的存在状态以各种方式对这些区域进行着色:

Porter-Duff

右上角和左下角矩形始终为空白,因为目标和源在这些区域中都是透明的。 目标颜色占据左上角区域,以便该区域可以用目标颜色着色,或者根本不用着色。 同样,源颜色占据右下角区域,以便该区域可以用源颜色着色,或者根本不用着色。 中间的目标和源的交集可以使用目标颜色、源颜色或完全不着色。

组合总数为 2(左上)乘以 2(右下)乘以 3(中心),即 12。 这些是 12 种基本的 Porter-Duff 合成模式。

在“合成数字图像”(第 256 页)的末尾,Porter 和 Duff 添加了第 13 种模式,称为 plus(对应于 SkiaSharp SKBlendMode.Plus 成员和 W3C Lighter 模式,请不要与 W3C Lighten 模式混淆。)Plus 模式添加目标颜色和源颜色,稍后将更详细地介绍该过程。

Skia 添加了名为 Modulate 的第 14 种模式,该模式与 Plus 非常相似,只是将目标颜色与源颜色相乘。 可将它视为附加的 Porter-Duff 混合模式。

下面是 SkiaSharp 中定义的 14 种 Porter-Duff 模式。 该表显示了它们如何为上图中的三个非空白区域着色:

模型 目标 交集 Source
Clear
Src Source X
Dst X 目标
SrcOver X Source X
DstOver X 目标 X
SrcIn
DstIn 目标
SrcOut X
DstOut X
SrcATop X
DstATop 目标 X
Xor X X
Plus X 求和 X
Modulate 产品

这些混合模式是对称的。 源和目标可以互换,所有模式仍然可用。

模式的命名约定遵循一些简单的规则:

  • Src 或 Dst 本身意味着只有源像素或目标像素可见
  • Over 后缀指示交集中显示的内容。 源或目标中的一个将绘制在另一个“之上”。
  • In 后缀表示仅着色交集。 输出仅限于源或目标中位于对方“内部”的部分。
  • Out 后缀表示交集不着色。 输出仅是源或目标在交集“外部”的部分。
  • ATop 后缀是 In 和 Out 的并集。它包括源或目标位于另一个“之上”的区域。

请注意 PlusModulate 模式的差别。 这些模式对源像素和目标像素执行不同类型的计算。 稍后将更详细地予以介绍。

“Porter-Duff 网格”页以网格形式在一个屏幕上显示所有 14 种模式。 每种模式都是 SKCanvasView 的单独实例。 因此,从 SKCanvasView 派生了一个名为 PorterDuffCanvasView 的类。 静态构造函数创建两个相同大小的位图,一个在其左上角区域有一个棕色矩形,另一个在其左上角区域有一个蓝色矩形:

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 重写绘制两个位图。 第一个是以正常方式绘制的:

canvas.DrawBitmap(dstBitmap, rect);

第二个是使用 SKPaint 对象绘制的,其中 BlendMode 属性已设置为构造函数参数:

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

OnPaintSurface 重写的其余部分在位图周围绘制一个矩形以指示它们的大小。

PorterDuffGridPage 类创建 14 个 PorterDurffCanvasView 实例,为 blendModes 数组的每个成员各创建一个。 数组中 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 方法的三个调用。 它们都使用无参数方法,将所有像素设置为透明:

canvas.Clear();

尝试更改这些调用中的任何一个,以便将像素设置为不透明白色:

canvas.Clear(SKColors.White);

做出这种更改后,有些混合模式似乎可以正常运行,但有些混合模式则不可以。 如果将源位图的背景设置为白色,则 SrcOver 模式将不起作用,因为源位图中没有透明像素可供目标显示。 如果将目标位图或画布的背景设置为白色,则 DstOver 不起作用,因为目标没有任何透明像素。

你可能忍不住使用更简单的 DrawRect 调用来替换“Porter-Duff 网格”页中的位图。 这适用于目标矩形,但不适用于源矩形。 源矩形必须不仅包含蓝色区域。 源矩形必须包含与目标的彩色区域对应的透明区域。 只有这样,这些混合模式才起作用。

通过 Porter-Duff 使用遮罩

“砖墙合成”页显示了经典合成任务的示例:一张图片需要由多个部分组合而成,其中包括需要消除背景的位图。 下面是背景有问题的 SeatedMonkey.jpg 位图

坐着的猴子

在准备合成过程中,创建了相应的遮罩,它是另一个位图,在你希望显示图像的位置为黑色,在其他位置则为透明。 此文件名为 SeatedMonkeyMatte.png,属于示例的 Media 文件夹中的资源之一

坐着的猴子 Matte

这不是专业创建的遮罩。 在理想情况下,遮罩应包括黑色像素边缘周围的部分透明像素,而此遮罩则不然。

“砖墙合成”页的 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>

代码隐藏文件加载所需的两个位图并处理 ButtonClicked 事件。 每次单击 Buttonstep 字段就会递增,并为 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 一次就会导致 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

再次按 Buttonstep 会递增为 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 噪声着色器:

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 类定义了三种类型的剪裁,分别对应于 ClipRectClipPathClipRegion 方法。 Porter-Duff 混合模式添加了另一种类型的剪裁,它允许将图像限制为可绘制的任何内容,包括位图。 “砖墙合成”中使用的遮罩本质上定义了一个剪裁区域

渐变透明度和过渡

本文前面所示的 Porter-Duff 混合模式示例均涉及到由不透明像素和透明像素组成的图像,但不包括部分透明的像素。 混合模式函数也是为这些像素定义的。 下表是 Porter-Duff 混合模式的更正式定义,它使用 Skia SkBlendMode 引用中的表示法。 (由于 SkBlendMode 引用是一种 Skia 引用,因此使用了 C++ 语法。)

从概念上讲,每个像素的红色、绿色、蓝色和 alpha 分量都从字节转换为 0 到 1 范围内的浮点数。 对于 alpha 通道,0 表示完全透明,1 表示完全不透明

下表中的表示法使用以下缩写:

  • Da 是目标 alpha 通道
  • Dc 是目标 RGB 颜色
  • Sa 是源 alpha 通道
  • Sc 是源 RGB 颜色

RGB 颜色预先乘以 alpha 值。 例如,如果 Sc 表示纯红色,但 Sa 为 0x80,则 RGB 颜色为 (0x80, 0, 0)。 如果 Sa 为 0,则所有 RGB 分量也为 0

结果显示在方括号中,其中 alpha 通道和 RGB 颜色以逗号分隔:[alpha, color]。 对于颜色,将分别对红色、绿色、蓝色分量执行计算:

模型 操作
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]

当 Da 和 Sa 为 0 或 1 时,这些运算更容易分析。 例如,对于默认的 SrcOver 模式,如果 Sa 为 0,则 Sc 也为 0,结果为 [Da, Dc],即目标 alpha 和颜色。 如果 Sa 为 1,则结果为 [Sa, Sc](源 alpha 和颜色)或 [1, Sc]

PlusModulate 模式与其他模式略有不同,因为源和目标的组合可以产生新的颜色。 Plus 模式可以用字节分量或浮点分量来解释。 在前面所示的“Porter-Duff 网格”页中,目标颜色为 (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 网格”页中交集处显示的颜色

在“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>

代码隐藏文件使用线性渐变来填充两个相同大小的矩形。 目标渐变方向为从右上到左下。 它在右上角是棕色的,但随后向中心开始淡化为透明,在左下角是透明的。

源矩形的渐变方向为从左上到右下。 左上角是蓝色的,但再次淡化为透明,右下角是透明的。

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 混合模式可以用于位图以外的图形对象。 但是,源必须包含透明区域。 此处的情况就是这样,因为渐变填充了矩形,但渐变的一部分是透明的。

下面是三个示例:

Porter-Duff 透明度

目标和源的配置与原始 Porter-Duff 合成数字图像论文第 255 页中所示的示意图非常相似,但此页表明混合模式对于部分透明度的区域表现非常好。

可以使用透明渐变来实现一些不同的效果。 一种可能性是蒙版,它类似于“SkiaSharp 圆形渐变”页的蒙版径向渐变部分中所示的技术。 “合成蒙版”页的大部分内容与之前的程序类似。 它加载位图资源并确定其显示所在的矩形。 根据预先确定的中心和半径创建径向渐变:

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 模式和部分透明渐变从一张图像过渡到另一张图像。 “渐变过渡”页包含一个 Slider,用于指示从 0 到 1 的过渡进度级别;其中还包含一个 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>

代码隐藏文件加载两个位图资源来演示过渡。 这与本文前面的“位图溶解”页中使用的两个图像相同。 该代码还定义了一个具有三个成员的枚举,这些成员对应于三种类型的渐变:线性、径向和扫掠。 这些值将加载到 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();
    }
    ···
}

代码隐藏文件创建三个 SKPaint 对象。 paint0 对象不使用混合模式。 此绘图对象用于绘制一个具有黑色到透明渐变的矩形,如 colors 数组中所示。 positions 数组基于 Slider 的位置,但进行了一些调整。 如果 Slider 采用最小值或最大值,则 progress 值为 0 或 1,并且两个位图之一应完全可见。 必须针对这些值相应地设置 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 的渐变为黑色。

colorsposition 数组均在用于创建渐变的 SKShader 的三个方法中使用。 根据 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 处理程序的最后四个语句中,显示了两个位图。 SrcOut 混合模式意味着第一个位图仅显示在背景的透明区域中。 第二位图的 DstOver 模式意味着第二位图仅在未显示第一位图的那些区域中显示。

以下屏幕截图显示了三种不同的过渡类型,每种类型带有 50% 标记:

渐变转换