不可分离混合模式

SkiaSharp 可分离混合模式一文中所述,可分离混合模式分别对红色、绿色和蓝色通道执行操作。 不可分离混合模式则不然。 通过对颜色的色调、饱和度和亮度级别进行操作,不可分离混合模式能够以有趣的方式改变颜色:

不可分离的示例

色调-饱和度-亮度模型

若要理解不可分离混合模式,需要将目标像素和源像素视为色调-饱和度-亮度模型中的颜色。 (亮度也称为光度。)

与 Xamarin.Forms 集成一文中讨论了 HSL 颜色模型,该文章中的示例程序允许对 HSL 颜色进行试验。 可以通过静态 SKColor.FromHsl 方法使用色调、饱和度和亮度值创建 SKColor 值。

色调代表颜色的主导波长。 色调值范围为 0 到 360,并在加法和减法原色之间循环:红色为值 0,黄色为 60,绿色为 120,青色为 180,蓝色为 240,品红色为 300,循环回到值为 360 的红色。

如果没有主导色(例如,颜色是白色或黑色,或者灰色阴影),则色调是未定义的,通常设置为 0。

饱和度值的范围为 0 到 100,表示颜色的纯度。 饱和度值为 100 是最纯的颜色,而低于 100 的值会导致颜色变得更灰。 饱和度值为 0 会产生灰色阴影。

亮度(或光度)值表示颜色的明亮程度。 无论其他设置如何,亮度值为 0 时都是黑色。 同样,亮度值为 100 时表示白色。

HSL 值 (0, 100, 50) 等效于 RGB 值 (FF, 00, 00),即纯红色。 HSL 值 (180, 100, 50) 等效于 RGB 值 (00, FF, FF),即纯青色。 随着饱和度降低,主导颜色成分会减少,而其他成分会增加。 饱和度级别为 0 时,所有分量都是相同的,并且颜色为灰色阴影。 降低亮度会变为黑色;增加亮度会变为白色。

混合模式详解

与其他混合模式一样,四种不可分离混合模式涉及到目标(通常是位图图像)和源(通常是单色或渐变)。 混合模式结合了来自目标和源的色调、饱和度和亮度值:

混合模式 源中的分量 目标中的分量
Hue Hue 饱和度和亮度
Saturation 饱和度 色调和亮度
Color 色调和饱和度 亮度
Luminosity 亮度 色调和饱和度

有关算法,请参阅 W3C 合成和混合级别 1 规范。

“不可分离混合模式”页包含用于选择这些混合模式之一的 Picker,以及三个用于选择 HSL 颜色的 Slider 视图:

<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.NonSeparableBlendModesPage"
             Title="Non-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.Hue" />
                    <x:Static Member="skia:SKBlendMode.Saturation" />
                    <x:Static Member="skia:SKBlendMode.Color" />
                    <x:Static Member="skia:SKBlendMode.Luminosity" />
                </x:Array>
            </Picker.ItemsSource>

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

        <Slider x:Name="hueSlider"
                Maximum="360"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="satSlider"
                Maximum="100"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="lumSlider"
                Maximum="100"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <StackLayout Orientation="Horizontal">
            <Label x:Name="hslLabel"
                   HorizontalOptions="CenterAndExpand" />

            <Label x:Name="rgbLabel"
                   HorizontalOptions="CenterAndExpand" />

        </StackLayout>
    </StackLayout>
</ContentPage>

为了节省空间,三个 Slider 视图未在程序的用户界面中标识。 需要记住顺序是色调、饱和度和亮度。 页面底部的两个 Label 视图显示了 HSL 和 RGB 颜色值。

代码隐藏文件加载位图资源之一,将其尽可能大地显示在画布上,然后用矩形覆盖画布。 矩形颜色基于三个 Slider 视图,混合模式是在 Picker 中选择的模式:

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

    public NonSeparableBlendModesPage()
    {
        InitializeComponent();
    }

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

    void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
    {
        // Calculate new color based on sliders
        color = SKColor.FromHsl((float)hueSlider.Value,
                                (float)satSlider.Value,
                                (float)lumSlider.Value);

        // Use labels to display HSL and RGB color values
        color.ToHsl(out float hue, out float sat, out float lum);

        hslLabel.Text = String.Format("HSL = {0:F0} {1:F0} {2:F0}",
                                      hue, sat, lum);

        rgbLabel.Text = String.Format("RGB = {0:X2} {1:X2} {2:X2}",
                                      color.Red, color.Green, color.Blue);

        canvasView.InvalidateSurface();
    }

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

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);

        // Get blend mode from Picker
        SKBlendMode blendMode =
            (SKBlendMode)(blendModePicker.SelectedIndex == -1 ?
                                        0 : blendModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Color = color;
            paint.BlendMode = blendMode;
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

请注意,程序不会显示三个滑块选择的 HSL 颜色值。 相反,它会根据这些滑块创建颜色值,然后使用 ToHsl 方法获取色调、饱和度和亮度值。 这是因为,FromHsl 方法将 HSL 颜色转换为 RGB 颜色,该颜色内部存储在 SKColor 结构中。 ToHsl 方法从 RGB 转换为 HSL,但结果并不始终原始值。

例如,FromHsl 会将 HSL 值 (180, 50, 0) 转换为 RGB 颜色 (0, 0, 0),因为 Luminosity 为零。 ToHsl 方法将 RGB 颜色 (0, 0, 0) 转换为 HSL 颜色 (0, 0, 0),因为色调和饱和度值不相关。 使用此程序时,最好查看程序正在使用的 HSL 颜色的表示形式,而不是使用滑块指定的颜色表示形式。

SKBlendModes.Hue 混合模式使用源的色调级别,同时保留目标的饱和度和亮度级别。 测试此混合模式时,饱和度和亮度滑块必须设置为 0 或 100 以外的值,因为在这种情况下,色调不是唯一定义的。

不可分离的混合模式 - 色调

将滑块设置为 0(如左侧的 iOS 屏幕截图所示)时,所有内容都会变成红色。 但这并不意味着图像完全没有绿色和蓝色。 显然,结果中仍然存在灰色阴影。 例如,RGB 颜色 (40, 40, C0) 等效于 HSL 颜色 (240, 50, 50)。 色调为蓝色,但饱和度值 50 表示也有红色和绿色分量。 如果使用 SKBlendModes.Hue 将色调设置为 0,则 HSL 颜色为 (0, 50, 50),即 RGB 颜色 (C0, 40, 40)。 仍然存在蓝色和绿色分量,但现在主导分量是红色。

SKBlendModes.Saturation 混合模式将源的饱和度级别与目标的色调和亮度相结合。 与色调一样,如果亮度为 0 或 100,则饱和度无法明确定义。 理论上,这两个极端值之间的任何亮度设置都有效。 但是,亮度设置似乎对结果的影响超出了应有的范围。 将亮度设置为 50,便可以知道如何设置图片的饱和度级别:

不可分离的混合模式 - 饱和度

可以使用此混合模式来增加暗淡图像的颜色饱和度,也可以将饱和度降低到零(如左侧的 iOS 屏幕截图所示)以获得仅由灰色阴影组成的最终图像。

SKBlendModes.Color 混合模式保留目标的亮度,但使用源的色调和饱和度。 同样,这意味着亮度滑块在极端值之间的任何设置都有效。

不可分离的混合模式 - 颜色

很快你就会看到这种混合模式的应用。

最后,SKBlendModes.Luminosity 混合模式与 SKBlendModes.Color 相反。 它保留目标的色调和饱和度,但使用源的亮度。 Luminosity 混合模式是批处理中最神秘的存在:色调和饱和度滑块会影响图像,但即使在中等亮度下,图像也不清晰:

不可分离的混合模式 - 亮度

理论上,增加或减少图像的亮度应该使其变亮或变暗。 请阅读此亮度属性示例或此 SKBlendMode 枚举定义

通常情况下,你不希望将其中一种不可分离混合模式与由应用于整个目标图像的单一颜色组成的源一起使用。 这种效果的影响范围太大。 需要将效果限制在图像的某一部分。 在这种情况下,源可能会包含透明度,或者源可能会仅限于较小的图形。

可分离模式的遮罩

下面是作为资源包含在示例中的位图之一。 文件名为 Banana.jpg

Banana Monkey

可以创建仅包含香蕉的遮罩。 这也是示例中的资源。 文件名为 BananaMatte.png

Banana Matte

除了黑色香蕉形状外,位图的其余部分都是透明的。

“蓝色香蕉”页使用该遮罩来改变猴子抓住的香蕉的色调和饱和度,但不改变图像中的其他内容

在以下 BlueBananaPage 类中,Banana.jpg 位图作为字段加载。 构造函数加载 BananaMatte.png 位图作为 matteBitmap 对象,但它不会在构造函数之外保留该对象。 相反,它会创建名为 blueBananaBitmap 的第三个位图。 matteBitmapblueBananaBitmap 上绘制,后接 SKPaint,其 Color 设置为蓝色,BlendMode 设置为 SKBlendMode.SrcInblueBananaBitmap 大部分保持透明,但带有香蕉的纯蓝色图像:

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

    SKBitmap blueBananaBitmap;

    public BlueBananaPage()
    {
        Title = "Blue Banana";

        // Load banana matte bitmap (black on transparent)
        SKBitmap matteBitmap = BitmapExtensions.LoadBitmapResource(
            typeof(BlueBananaPage),
            "SkiaSharpFormsDemos.Media.BananaMatte.png");

        // Create a bitmap with a solid blue banana and transparent otherwise
        blueBananaBitmap = new SKBitmap(matteBitmap.Width, matteBitmap.Height);

        using (SKCanvas canvas = new SKCanvas(blueBananaBitmap))
        {
            canvas.Clear();
            canvas.DrawBitmap(matteBitmap, new SKPoint(0, 0));

            using (SKPaint paint = new SKPaint())
            {
                paint.Color = SKColors.Blue;
                paint.BlendMode = SKBlendMode.SrcIn;
                canvas.DrawPaint(paint);
            }
        }

        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();

        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);

        using (SKPaint paint = new SKPaint())
        {
            paint.BlendMode = SKBlendMode.Color;
            canvas.DrawBitmap(blueBananaBitmap,
                              info.Rect,
                              BitmapStretch.Uniform,
                              paint: paint);
        }
    }
}

PaintSurface 处理程序绘制猴子抓住香蕉的位图。 此代码随后使用 SKBlendMode.Color 显示 blueBananaBitmap。 在香蕉的表面上,每个像素的色调和饱和度都由纯蓝色替换,对应于色调值 240 和饱和度值 100。 但是,亮度保持不变,这意味着香蕉尽管具有新颜色,但仍然具有逼真的纹理:

Blue Banana

尝试将混合模式更改为 SKBlendMode.Saturation。 香蕉仍然是黄色的,但黄色更深。 在现实应用中,这样产生的效果可能比将香蕉变成蓝色更理想。