SkiaSharp 圆形渐变

SKShader 类定义静态方法来创建四种不同类型的渐变。 SkiaSharp 线性渐变一文讨论了 CreateLinearGradient 方法。 本文介绍其他三种类型的渐变,它们都基于圆。

CreateRadialGradient 方法创建一个从圆心产生的渐变:

径向渐变示例

CreateSweepGradient 方法创建围绕圆心扫掠的渐变:

扫掠渐变示例

第三种渐变类型很不常见。 它称为两点圆锥渐变,由 CreateTwoPointConicalGradient 方法定义。 渐变从一个圆延伸到另一个圆:

圆锥渐变示例

如果两个圆的大小不同,则渐变呈圆锥形。

本文将更详细地探讨这些渐变。

径向渐变

CreateRadialGradient 方法采用以下语法:

public static SKShader CreateRadialGradient (SKPoint center,
                                             Single radius,
                                             SKColor[] colors,
                                             Single[] colorPos,
                                             SKShaderTileMode mode)

CreateRadialGradient 重载还包括变换矩阵参数。

前两个参数指定圆心和半径。 渐变从该中心开始,向外延伸 radius 个像素。 radius 之外发生的情况取决于 SKShaderTileMode 参数。 colors 参数是两种或更多种颜色的数组(就像线性渐变方法中一样),而 colorPos 是 0 到 1 范围内的整数数组。 这些整数表示颜色沿 radius 线的相对位置。 可以将该参数设置为 null 以均匀分布颜色。

如果使用 CreateRadialGradient 填充圆,则可以将渐变的中心设置为圆心,将渐变的半径设置为圆半径。 在这种情况下,SKShaderTileMode 参数对渐变的渲染没有影响。 但是,如果渐变填充的区域大于渐变定义的圆,则 SKShaderTileMode 参数会对圆外发生的情况产生深远影响。

示例中的“径向渐变”页演示了 SKShaderMode 的效果。 此页的 XAML 文件实例化一个 Picker,允许选择 SKShaderTileMode 枚举的三个成员之一:

<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:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.RadialGradientPage"
             Title="Radial Gradient">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <skiaforms:SKCanvasView x:Name="canvasView"
                                Grid.Row="0"
                                PaintSurface="OnCanvasViewPaintSurface" />

        <Picker x:Name="tileModePicker"
                Grid.Row="1"
                Title="Shader Tile Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKShaderTileMode}">
                    <x:Static Member="skia:SKShaderTileMode.Clamp" />
                    <x:Static Member="skia:SKShaderTileMode.Repeat" />
                    <x:Static Member="skia:SKShaderTileMode.Mirror" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</ContentPage>

代码隐藏文件使用径向渐变为整个画布着色。 渐变的中心设置为画布的中心,半径设置为 100 像素。 渐变仅包含两种颜色,即黑色和白色:

public partial class RadialGradientPage : ContentPage
{
    public RadialGradientPage ()
    {
        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();

        SKShaderTileMode tileMode =
            (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
                                        0 : tileModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateRadialGradient(
                                new SKPoint(info.Rect.MidX, info.Rect.MidY),
                                100,
                                new SKColor[] { SKColors.Black, SKColors.White },
                                null,
                                tileMode);

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

此代码创建一个以黑色为中心的渐变,在距中心 100 像素处逐渐淡化为白色。 超出该半径会发生什么情况取决于 SKShaderTileMode 参数:

径向渐变

在所有三种情况下,渐变都会填充画布。 在左侧的 iOS 屏幕上,超出半径的渐变继续使用最后一种颜色,即白色。 这就是 SKShaderTileMode.Clamp 的结果。 Android 屏幕显示 SKShaderTileMode.Repeat 的效果:在距离中心 100 像素处,渐变再次从第一种颜色开始,即黑色。 渐变每 100 个像素半径重复一次。

右侧的通用 Windows 平台屏幕显示了 SKShaderTileMode.Mirror 如何使渐变方向交替。 第一个渐变是从中心的黑色到半径 100 像素的白色。 接下来是从 100 像素半径处的白色到 200 像素半径处的黑色,而下一个渐变再次反转。

可以在径向渐变中使用两种以上的颜色。 “彩虹弧渐变”示例创建一个包含八种颜色的数组(对应于彩虹的颜色并以红色结尾),以及一个包含八个位置值的数组

public class RainbowArcGradientPage : ContentPage
{
    public RainbowArcGradientPage ()
    {
        Title = "Rainbow Arc Gradient";

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

        using (SKPaint paint = new SKPaint())
        {
            float rainbowWidth = Math.Min(info.Width, info.Height) / 4f;

            // Center of arc and gradient is lower-right corner
            SKPoint center = new SKPoint(info.Width, info.Height);

            // Find outer, inner, and middle radius
            float outerRadius = Math.Min(info.Width, info.Height);
            float innerRadius = outerRadius - rainbowWidth;
            float radius = outerRadius - rainbowWidth / 2;

            // Calculate the colors and positions
            SKColor[] colors = new SKColor[8];
            float[] positions = new float[8];

            for (int i = 0; i < colors.Length; i++)
            {
                colors[i] = SKColor.FromHsl(i * 360f / 7, 100, 50);
                positions[i] = (i + (7f - i) * innerRadius / outerRadius) / 7f;
            }

            // Create sweep gradient based on center and outer radius
            paint.Shader = SKShader.CreateRadialGradient(center,
                                                         outerRadius,
                                                         colors,
                                                         positions,
                                                         SKShaderTileMode.Clamp);
            // Draw a circle with a wide line
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = rainbowWidth;

            canvas.DrawCircle(center, radius, paint);
        }
    }
}

假设画布的宽度和高度最小值为 1000,这意味着 rainbowWidth 值为 250。 outerRadiusinnerRadius 值分别设置为 1000 和 750。 这些值用于计算 positions 数组;八个值的范围为 0.75f 到 1。 radius 值用于画圆。 值 875 表示 250 像素笔划宽度在 750 像素半径和 1000 像素半径之间延伸:

彩虹弧渐变

如果用此渐变填充整个画布,你会看到它在内半径内是红色的。 这是因为 positions 数组不以 0 开头。 第一种颜色用于从 0 到第一个数组值的偏移。 超出外半径的渐变也是红色的。 这是 Clamp 平铺模式的结果。 由于渐变用于画粗线,因此这些红色区域不可见。

用于遮罩的径向渐变

与线性渐变一样,径向渐变可以包含透明或部分透明的颜色。 此功能对于称为“遮罩”的过程很有用,该过程隐藏图像的一部分以突出图像的另一部分

“径向渐变遮罩”页显示了一个示例。 程序加载资源位图之一。 CENTERRADIUS 字段是通过检查位图确定的,引用应突出显示的区域。 PaintSurface 处理程序首先计算一个用于显示位图的矩形,然后将其显示在该矩形中:

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

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

    public RadialGradientMaskPage ()
    {
        Title = "Radial Gradient 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.Transparent,
                                                SKColors.White },
                                new float[] { 0.6f, 1 },
                                SKShaderTileMode.Clamp);

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

绘制位图后,一些简单的代码将 CENTERRADIUS 转换为 centerradius,它们引用位图中已缩放和移动的、要显示的突出显示区域。 这些值用于创建具有该中心和半径的径向渐变。 这两种颜色从中心和半径的前 60% 开始是透明的。 然后渐变淡化为白色:

径向渐变蒙版

此方法不是遮罩位图的最佳方法。 问题是遮罩的颜色大多为白色,选择白色是为了与画布的背景相匹配。 如果背景是其他颜色 – 或者可能是渐变本身 – 它将不匹配。 SkiaSharp Porter-Duff 混合模式一文中介绍了一种更好的遮罩方法。

镜面高光的径向渐变

当光线照射到圆形表面时,它会向多个方向反射光线,但有些光线会直接反射到观看者的眼睛中。 这通常会在表面上产生模糊的白色区域,称为“镜面高光”

在三维图形中,镜面高光通常是由用于确定光路径和阴影的算法产生的。 在二维图形中,有时会添加镜面高光来暗示 3D 表面的外观。 镜面高光可以将扁平的红色圆转变为圆形的红色球。

“径向镜面高光”页使用径向渐变来实现这一目的PaintSurface 处理程序首先计算圆的半径和两个 SKPoint 值 — 一个 center 和一个位于圆的中心和左上角边缘中间的 offCenter

public class RadialSpecularHighlightPage : ContentPage
{
    public RadialSpecularHighlightPage()
    {
        Title = "Radial Specular Highlight";

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

        float radius = 0.4f * Math.Min(info.Width, info.Height);
        SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
        SKPoint offCenter = center - new SKPoint(radius / 2, radius / 2);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateRadialGradient(
                                offCenter,
                                radius / 2,
                                new SKColor[] { SKColors.White, SKColors.Red },
                                null,
                                SKShaderTileMode.Clamp);

            canvas.DrawCircle(center, radius, paint);
        }
    }
}

CreateRadialGradient 调用创建一个渐变,该渐变从 offCenter 点开始为白色,到半径一半的距离处为红色结束。 它的外观如下所示:

径向镜面高光

如果你仔细观察此渐变,可能会认为它有缺陷。 渐变以特定点为中心,你可能希望它不是太对称以反射圆形表面。 在这种情况下,你可能更偏好下面镜面高光的锥形渐变部分中所示的镜面高光。

扫掠渐变

CreateSweepGradient 方法使用所有渐变创建方法中最简单的语法:

public static SKShader CreateSweepGradient (SKPoint center,
                                            SKColor[] colors,
                                            Single[] colorPos)

它只是一个中心、颜色数组和颜色位置。 渐变从中心点右侧开始,绕中心顺时针扫掠 360 度。 请注意,没有 SKShaderTileMode 参数。

还可以使用带有矩阵变换参数的 CreateSweepGradient 重载。 可以对渐变应用旋转变换来更改起点。 还可以应用比例变换以将方向从顺时针更改为逆时针。

“扫掠渐变”页使用扫掠渐变为笔划宽度为 50 像素的圆圈着色

扫掠渐变

SweepGradientPage 类定义了具有不同色调值的八种颜色的数组。 请注意,数组以红色开头和结尾(色调值为 0 或 360),它出现在屏幕截图的最右侧:

public class SweepGradientPage : ContentPage
{
    bool drawBackground;

    public SweepGradientPage ()
    {
        Title = "Sweep Gradient";

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

        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            drawBackground ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);
    }

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

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Define an array of rainbow colors
            SKColor[] colors = new SKColor[8];

            for (int i = 0; i < colors.Length; i++)
            {
                colors[i] = SKColor.FromHsl(i * 360f / 7, 100, 50);
            }

            SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);

            // Create sweep gradient based on center of canvas
            paint.Shader = SKShader.CreateSweepGradient(center, colors, null);

            // Draw a circle with a wide line
            const int strokeWidth = 50;
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = strokeWidth;

            float radius = (Math.Min(info.Width, info.Height) - strokeWidth) / 2;
            canvas.DrawCircle(center, radius, paint);

            if (drawBackground)
            {
                // Draw the gradient on the whole canvas
                paint.Style = SKPaintStyle.Fill;
                canvas.DrawRect(info.Rect, paint);
            }
        }
    }
}

程序还实现一个 TapGestureRecognizer 用于启用 PaintSurface 处理程序末尾的一些代码。 此代码使用相同的渐变来填充画布:

扫掠渐变全程

这些屏幕截图演示了渐变填充它所着色的任何区域。 如果渐变的开始和结束颜色不同,则中心点右侧将会出现不连续性。

两点圆锥渐变

CreateTwoPointConicalGradient 方法采用以下语法:

public static SKShader CreateTwoPointConicalGradient (SKPoint startCenter,
                                                      Single startRadius,
                                                      SKPoint endCenter,
                                                      Single endRadius,
                                                      SKColor[] colors,
                                                      Single[] colorPos,
                                                      SKShaderTileMode mode)

这些参数以两个圆的中心点和半径开始,称为起始圆和结束圆。 其余三个参数与 CreateLinearGradientCreateRadialGradient 相同。 CreateTwoPointConicalGradient 重载包括矩阵变换。

渐变从起始圆开始,到结束圆结束。 SKShaderTileMode 参数控制两个圆之外发生的情况。 两点锥形渐变是唯一不完全填充区域的渐变。 如果两个圆的半径相同,则渐变将限制为宽度与圆的直径相同的矩形。 如果两个圆的半径不同,则渐变形成一个圆锥体。

你可能想要尝试使用两点圆锥渐变,因此“圆锥渐变”页源自 InteractivePage,允许两个触摸点围绕两个圆半径移动:

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
                       xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Effects.ConicalGradientPage"
                       Title="Conical Gradient">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid BackgroundColor="White"
              Grid.Row="0">
            <skiaforms:SKCanvasView x:Name="canvasView"
                                    PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

        <Picker x:Name="tileModePicker"
                Grid.Row="1"
                Title="Shader Tile Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKShaderTileMode}">
                    <x:Static Member="skia:SKShaderTileMode.Clamp" />
                    <x:Static Member="skia:SKShaderTileMode.Repeat" />
                    <x:Static Member="skia:SKShaderTileMode.Mirror" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</local:InteractivePage>

代码隐藏文件定义了两个固定半径为 50 和 100 的 TouchPoint 对象:

public partial class ConicalGradientPage : InteractivePage
{
    const int RADIUS1 = 50;
    const int RADIUS2 = 100;

    public ConicalGradientPage ()
    {
        touchPoints = new TouchPoint[2];

        touchPoints[0] = new TouchPoint
        {
            Center = new SKPoint(100, 100),
            Radius = RADIUS1
        };

        touchPoints[1] = new TouchPoint
        {
            Center = new SKPoint(300, 300),
            Radius = RADIUS2
        };

        InitializeComponent();
        baseCanvasView = canvasView;
    }

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

        SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
        SKShaderTileMode tileMode =
            (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
                                        0 : tileModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateTwoPointConicalGradient(touchPoints[0].Center,
                                                                  RADIUS1,
                                                                  touchPoints[1].Center,
                                                                  RADIUS2,
                                                                  colors,
                                                                  null,
                                                                  tileMode);
            canvas.DrawRect(info.Rect, paint);
        }

        // Display the touch points here rather than by TouchPoint
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 3;

            foreach (TouchPoint touchPoint in touchPoints)
            {
                canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
            }
        }
    }
}

colors 数组为红色、绿色和蓝色。 PaintSurface 处理程序底部的代码将两个触摸点绘制为黑色圆,以便它们不会阻碍渐变。

请注意,DrawRect 调用使用渐变来为整个画布着色。 但是,在一般情况下,画布的大部分仍然没有被渐变着色。 下面是显示三种可能配置的程序:

圆锥渐变

左侧的 iOS 屏幕显示了 ClampSKShaderTileMode 设置的效果。 渐变从与最接近第二个圆的一侧相对的较小圆的边缘内部的红色开始。 Clamp 值还会导致红色持续到圆锥点。 渐变在最接近第一个圆的较大圆的外边缘处以蓝色结束,但在该圆内及之外继续以蓝色继续。

Android 屏幕类似,但 SKShaderTileModeRepeat。 现在更清楚的是,渐变从第一个圆内开始,在第二个圆外结束。 Repeat 设置会导致渐变再次重复,并在较大的圆内显示红色。

UWP 屏幕显示当较小的圆完全移动到较大的圆内时会发生什么情况。 渐变不再是圆锥,而是填充整个区域。 效果与径向渐变类似,但如果较小的圆没有完全位于较大圆的中心,则效果是不对称的。

当一个圆嵌套在另一个圆中时,你可能会怀疑渐变的实际用途,但它对于镜面高光而言是理想的选择。

镜面高光的锥形渐变

在本文前面,你已了解如何使用径向渐变来创建镜面高光。 还可以使用两点锥形渐变来实现此目的,并且你可能对它的外观有偏好:

圆锥镜面高光

非对称外观更好地暗示了物体的圆形表面。

除着色器外,“锥形镜面高光”页中的绘制代码与“径向镜面高光”页相同

public class ConicalSpecularHighlightPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateTwoPointConicalGradient(
                                offCenter,
                                1,
                                center,
                                radius,
                                new SKColor[] { SKColors.White, SKColors.Red },
                                null,
                                SKShaderTileMode.Clamp);

            canvas.DrawCircle(center, radius, paint);
        }
    }
}

这两个圆的圆心分别为 offCentercenter。 以 center 为中心的圆与包围整个球的半径相关联,但以 offCenter 为中心的圆的半径仅为一个像素。 渐变实际上从该点开始,在球的边缘结束。