可分离混合模式

如文章 SkiaSharp Porter-Duff 混合模式中所述,Porter-Duff 混合模式通常执行剪裁操作。 可分离混合模式与此不同。 可分离模式改变图像的各个红色、绿色和蓝色分量。 可分离混合模式可以混合颜色以证明红色、绿色和蓝色的组合确实是白色:

原色

变亮和变暗两种方式

位图有点太暗或太亮是很常见的。 可以使用可分离混合模式来使图像变亮或变暗。 事实上,SKBlendMode 枚举中的两个可分离混合模式名为 LightenDarken

“变亮和变暗”页演示了这两种模式。 XAML 文件实例化两个 SKCanvasView 对象和两个 Slider 视图:

<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.LightenAndDarkenPage"
             Title="Lighten and Darken">
    <StackLayout>
        <skia:SKCanvasView x:Name="lightenCanvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="lightenSlider"
                Margin="10"
                ValueChanged="OnSliderValueChanged" />

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

        <Slider x:Name="darkenSlider"
                Margin="10"
                ValueChanged="OnSliderValueChanged" />
    </StackLayout>
</ContentPage>

第一个 SKCanvasViewSlider 演示 SKBlendMode.Lighten,第二对演示 SKBlendMode.Darken。 两个 Slider 视图共享同一个 ValueChanged 处理程序,两个 SKCanvasView 共享同一个 PaintSurface 处理程序。 这两个事件处理程序都会检查哪个对象正在触发事件:

public partial class LightenAndDarkenPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                typeof(SeparableBlendModesPage),
                "SkiaSharpFormsDemos.Media.Banana.jpg");

    public LightenAndDarkenPage ()
    {
        InitializeComponent ();
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        if ((Slider)sender == lightenSlider)
        {
            lightenCanvasView.InvalidateSurface();
        }
        else
        {
            darkenCanvasView.InvalidateSurface();
        }
    }

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

        canvas.Clear();

        // Find largest size rectangle in canvas
        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
        canvas.DrawBitmap(bitmap, rect);

        // Display gray rectangle with blend mode
        using (SKPaint paint = new SKPaint())
        {
            if ((SKCanvasView)sender == lightenCanvasView)
            {
                byte value = (byte)(255 * lightenSlider.Value);
                paint.Color = new SKColor(value, value, value);
                paint.BlendMode = SKBlendMode.Lighten;
            }
            else
            {
                byte value = (byte)(255 * (1 - darkenSlider.Value));
                paint.Color = new SKColor(value, value, value);
                paint.BlendMode = SKBlendMode.Darken;
            }

            canvas.DrawRect(rect, paint);
        }
    }
}

PaintSurface 处理程序计算适合位图的矩形。 该处理程序显示该位图,然后使用 SKPaint 对象(其 BlendMode 属性设置为 SKBlendMode.LightenSKBlendMode.Darken)在该位图上显示一个矩形。 Color 属性是基于 Slider 的灰色阴影。 对于 Lighten 模式,颜色范围为黑色到白色,而对于 Darken 模式,颜色范围为白色到黑色。

从左到右的屏幕截图显示,随着顶部图像变亮而底部图像变暗,Slider 值越来越大:

变亮和变暗

此程序演示了使用可分离混合模式的正常方式:目标是某种图像,通常是位图。 源是使用 SKPaint 对象(其 BlendMode 属性设置为可分离混合模式)显示的矩形。 该矩形可以是纯色(如此处所示)或渐变。 透明度通常不与可分离混合模式一起使用

当体验此程序时,你会发现这两种混合模式不会均匀地使图像变亮和变暗。 相反,Slider 似乎设置了某种阈值。 例如,当增加 Lighten 模式的 Slider 时,图像的较暗区域首先变亮,而较亮区域保持不变。

对于 Lighten 模式,如果目标像素为 RGB 颜色值 (Dr, Dg, Db),源像素为颜色 (Sr, Sg, Sb),则输出为如下所示计算出的 (Or, Og, Ob):

Or = max(Dr, Sr) Og = max(Dg, Sg) Ob = max(Db, Sb)

对于红色、绿色和蓝色,结果是目标与源中较大的一个。 这会产生首先照亮目标的黑暗区域的效果。

Darken 模式与此类似,只是结果是目标和源中较小的一个:

Or = min(Dr, Sr) Og = min(Dg, Sg) Ob = min(Db, Sb)

红色、绿色和蓝色分量分别单独处理,因此这些混合模式被称为可分离混合模式。 因此,Dc 和 Sc 缩写可用于目标颜色和源颜色,并且计算分别适用于红色、绿色和蓝色分量

下表显示了所有可分离混合模式,并简要说明了它们的作用。 第二列显示不产生任何变化的源颜色:

混合模式 没有变化 操作
Plus 通过将颜色相加来变亮:Sc + Dc
Modulate 白色 通过将颜色相乘来变暗:Sc·Dc
Screen 补足乘积:Sc + Dc – Sc·Dc
Overlay 灰色 HardLight 的逆
Darken 白色 颜色最小值:min(Sc, Dc)
Lighten 颜色最大值:max(Sc, Dc)
ColorDodge 根据源使目标变亮
ColorBurn 白色 根据源使目标变暗
HardLight 灰色 类似于刺眼聚光灯的效果
SoftLight 灰色 类似于柔光聚光灯的效果
Difference 从较亮部分减去较暗部分:Abs(Dc – Sc)
Exclusion 类似于 Difference 但对比度更低
Multiply 白色 通过将颜色相乘来变暗:Sc·Dc

更详细的算法可以在 W3C 合成与混合级别 1 规范和 Skia SkBlendMode 参考中找到,不过这两个源中的表示法并不相同。 请记住,Plus 通常被视为 Porter-Duff 混合模式,而 Modulate 不是 W3C 规范的一部分。

如果源是透明的,则对于除 Modulate 之外的所有可分离混合模式,混合模式都不起作用。 如前所述,Modulate 混合模式在乘法中合并 alpha 通道。 否则,ModulateMultiply 具有相同的效果。

请注意名为 ColorDodgeColorBurn 的两种模式。 “局部遮光”和“炽烤”这两个词起源于摄影暗室实践。 放大镜通过将光线照射到底片上来制作影印件。 在没有光线的情况下,影印件是白色的。 随着更多的光线照射到影印件上并持续更长时间,影印件会变得更暗。 影印制作者经常用手或小物体阻挡部分光线落在影印件的某个部分,使该区域变亮。 这称为局部遮光。 相反,可以使用带孔的不透明材料(或用手挡住大部分光线)将更多光线引导至到特定位置,使其变暗,这称为炽烤

局部遮光和炽烤程序与变亮和变暗非常相似。 XAML 文件的结构相同,但元素名称不同,代码隐藏文件也非常相似,但这两种混合模式的效果却截然不同:

减淡和加深

对于较小的 Slider 值,Lighten 模式首先使暗区域变亮,而 ColorDodge 则变亮得更均匀。

图像处理应用程序通常允许将局部遮光和炽烤限制在特定区域,就像在暗室中一样。 这可以通过渐变或具有不同灰度的位图来完成。

探索可分离混合模式

在“可分离混合模式”页中,可以检查所有可分离混合模式。 它使用其中一种混合模式显示位图目标和彩色矩形源。

XAML 文件定义了一个 Picker(用于选择混合模式)和四个滑块。 前三个滑块可让你设置源的红色、绿色和蓝色分量。 第四个滑块旨在通过设置灰色阴影来重写这些值。 各个滑块未识别,但颜色指示其功能:

<?xml version="1.0" encoding="utf-8" ?>
<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.SeparableBlendModesPage"
             Title="Separable Blend Modes">

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

        <Picker x:Name="blendModePicker"
                Title="Blend Mode"
                Margin="10, 0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKBlendMode}">
                    <x:Static Member="skia:SKBlendMode.Plus" />
                    <x:Static Member="skia:SKBlendMode.Modulate" />
                    <x:Static Member="skia:SKBlendMode.Screen" />
                    <x:Static Member="skia:SKBlendMode.Overlay" />
                    <x:Static Member="skia:SKBlendMode.Darken" />
                    <x:Static Member="skia:SKBlendMode.Lighten" />
                    <x:Static Member="skia:SKBlendMode.ColorDodge" />
                    <x:Static Member="skia:SKBlendMode.ColorBurn" />
                    <x:Static Member="skia:SKBlendMode.HardLight" />
                    <x:Static Member="skia:SKBlendMode.SoftLight" />
                    <x:Static Member="skia:SKBlendMode.Difference" />
                    <x:Static Member="skia:SKBlendMode.Exclusion" />
                    <x:Static Member="skia:SKBlendMode.Multiply" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <Slider x:Name="redSlider"
                MinimumTrackColor="Red"
                MaximumTrackColor="Red"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="greenSlider"
                MinimumTrackColor="Green"
                MaximumTrackColor="Green"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="blueSlider"
                MinimumTrackColor="Blue"
                MaximumTrackColor="Blue"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="graySlider"
                MinimumTrackColor="Gray"
                MaximumTrackColor="Gray"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label x:Name="colorLabel"
               HorizontalTextAlignment="Center" />

    </StackLayout>
</ContentPage>

代码隐藏文件加载位图资源之一并将其绘制两次,一次在画布的上半部分,另一次在画布的下半部分:

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

    public SeparableBlendModesPage()
    {
        InitializeComponent();
    }

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

    void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
    {
        if (sender == graySlider)
        {
            redSlider.Value = greenSlider.Value = blueSlider.Value = graySlider.Value;
        }

        colorLabel.Text = String.Format("Color = {0:X2} {1:X2} {2:X2}",
                                        (byte)(255 * redSlider.Value),
                                        (byte)(255 * greenSlider.Value),
                                        (byte)(255 * blueSlider.Value));

        canvasView.InvalidateSurface();
    }

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

        canvas.Clear();

        // Draw bitmap in top half
        SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
        canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);

        // Draw bitmap in bottom halr
        rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
        canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);

        // Get values from XAML controls
        SKBlendMode blendMode =
            (SKBlendMode)(blendModePicker.SelectedIndex == -1 ?
                                        0 : blendModePicker.SelectedItem);

        SKColor color = new SKColor((byte)(255 * redSlider.Value),
                                    (byte)(255 * greenSlider.Value),
                                    (byte)(255 * blueSlider.Value));

        // Draw rectangle with blend mode in bottom half
        using (SKPaint paint = new SKPaint())
        {
            paint.Color = color;
            paint.BlendMode = blendMode;
            canvas.DrawRect(rect, paint);
        }
    }
}

PaintSurface 处理程序的底部,使用选定的混合模式和选定的颜色在第二个位图上绘制一个矩形。 可以将底部的修改后位图与顶部的原始位图进行比较:

可分离的混合模式

加法和减法原色

“原色页”绘制了三个重叠的红色、绿色和蓝色圆

附加原色

这些是加法原色。 任意两种颜色的组合会产生青色、品红色和黄色,所有三种颜色的组合会产生白色。

这三个圆是使用 SKBlendMode.Plus 模式绘制的,但你也可以使用 ScreenLightenDifference 来达到相同的效果。 以下是程序:

public class PrimaryColorsPage : ContentPage
{
    bool isSubtractive;

    public PrimaryColorsPage ()
    {
        Title = "Primary Colors";

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

        // Switch between additive and subtractive primaries at tap
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            isSubtractive ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);

        Content = canvasView;
    }

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

        canvas.Clear();

        SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
        float radius = Math.Min(info.Width, info.Height) / 4;
        float distance = 0.8f * radius;     // from canvas center to circle center
        SKPoint center1 = center + 
            new SKPoint(distance * (float)Math.Cos(9 * Math.PI / 6),
                        distance * (float)Math.Sin(9 * Math.PI / 6));
        SKPoint center2 = center +
            new SKPoint(distance * (float)Math.Cos(1 * Math.PI / 6),
                        distance * (float)Math.Sin(1 * Math.PI / 6));
        SKPoint center3 = center +
            new SKPoint(distance * (float)Math.Cos(5 * Math.PI / 6),
                        distance * (float)Math.Sin(5 * Math.PI / 6));

        using (SKPaint paint = new SKPaint())
        {
            if (!isSubtractive)
            {
                paint.BlendMode = SKBlendMode.Plus; 
                System.Diagnostics.Debug.WriteLine(paint.BlendMode);

                paint.Color = SKColors.Red;
                canvas.DrawCircle(center1, radius, paint);

                paint.Color = SKColors.Lime;    // == (00, FF, 00)
                canvas.DrawCircle(center2, radius, paint);

                paint.Color = SKColors.Blue;
                canvas.DrawCircle(center3, radius, paint);
            }
            else
            {
                paint.BlendMode = SKBlendMode.Multiply
                System.Diagnostics.Debug.WriteLine(paint.BlendMode);

                paint.Color = SKColors.Cyan;
                canvas.DrawCircle(center1, radius, paint);

                paint.Color = SKColors.Magenta;
                canvas.DrawCircle(center2, radius, paint);

                paint.Color = SKColors.Yellow;
                canvas.DrawCircle(center3, radius, paint);
            }
        }
    }
}

该程序包括一个 TabGestureRecognizer。 点击或单击屏幕时,该程序会使用 SKBlendMode.Multiply 显示三种减法原色:

减色原色

Darken 模式也可以达到同样的效果。