Поделиться через


Линейный градиент SkiaSharp

Класс SKPaint определяет Color свойство, которое используется для штрихов или заливки областей сплошным цветом. Кроме того, линии штрихов или области заливки с градиентами, которые являются постепенными смешения цветов:

Пример линейного градиента

Самый простой тип градиента — линейный градиент. Сочетание цветов происходит на линии (называемой градиентной линией) от одной точки к другой. Линии, которые являются перпендикулярными к линии градиента, имеют тот же цвет. Вы создаете линейный градиент с помощью одного из двух статических SKShader.CreateLinearGradient методов. Разница между двумя перегрузками заключается в том, что один включает преобразование матрицы, а другой — нет.

Эти методы возвращают объект типа SKShader , заданного свойством Shader SKPaint. Shader Если свойство не равно null, оно переопределяет Color свойство. Любая линия, заполненная или заполненная этой SKPaint областью, основана на градиенте, а не на сплошном цвете.

Примечание.

Свойство Shader игнорируется при включении SKPaint объекта в DrawBitmap вызов. Свойство можно использовать Color SKPaint для задания уровня прозрачности для отображения растрового изображения (как описано в статье Displaying SkiaSharp bitmaps), но нельзя использовать Shader свойство для отображения растрового изображения с градиентной прозрачностью. Другие методы доступны для отображения растровых изображений с градиентными прозрачностью: эти методы описаны в статьях SkiaSharp циклические градиенты и режимы компостирования SkiaSharp.

Градиенты по углам

Часто линейный градиент расширяется от одного угла прямоугольника к другому. Если начальная точка — левый верхний угол прямоугольника, градиент может расшириться:

  • вертикально к левому нижнему углу
  • горизонтально к правому верхнему углу
  • диагонали к правому нижнему углу

Диагонали линейный градиент показан на первой странице в разделе шейдеров SkiaSharp и других эффектов примера. Страница Градиента "Угол в угол" создает в SKCanvasView конструкторе. Обработчик PaintSurface создает SKPaint объект в using инструкции, а затем определяет прямоугольник 300 пикселей, центрированные на холсте:

public class CornerToCornerGradientPage : ContentPage
{
    ···
    public CornerToCornerGradientPage ()
    {
        Title = "Corner-to-Corner 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())
        {
            // Create 300-pixel square centered rectangle
            float x = (info.Width - 300) / 2;
            float y = (info.Height - 300) / 2;
            SKRect rect = new SKRect(x, y, x + 300, y + 300);

            // Create linear gradient from upper-left to lower-right
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Left, rect.Top),
                                new SKPoint(rect.Right, rect.Bottom),
                                new SKColor[] { SKColors.Red, SKColors.Blue },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Repeat);

            // Draw the gradient on the rectangle
            canvas.DrawRect(rect, paint);
            ···
        }
    }
}

Свойство Shader SKPaint присваивается SKShader возвращаемого значения из статического SKShader.CreateLinearGradient метода. Пять аргументов приведены следующим образом:

  • Начальная точка градиента, установленная здесь в левом верхнем углу прямоугольника.
  • Конечная точка градиента, установленная здесь в правом нижнем углу прямоугольника.
  • Массив двух или более цветов, которые способствуют градиенту
  • Массив значений, указывающий float относительное положение цветов в линии градиента
  • Элемент SKShaderTileMode перечисления, указывающий, как градиент работает за пределами конца линии градиента.

После создания DrawRect объекта градиента метод рисует прямоугольник 300 пикселей с помощью SKPaint объекта, включающего шейдер. Здесь он работает в iOS, Android и универсальная платформа Windows (UWP):

Градиент по углам

Линия градиента определяется двумя точками, указанными в качестве первых двух аргументов. Обратите внимание, что эти точки относятся к холсту , а не к графическому объекту, отображаемого с градиентом. Вдоль градиентной линии цвет постепенно переходит от красного в левом верхнем углу к синему в правом нижнем углу. Любая линия, которая является перпендикулярной к градиентной линии, имеет постоянный цвет.

Массив значений, указанных в качестве четвертого аргумента float , имеет одно-одно соответствие с массивом цветов. Значения указывают относительную позицию вдоль линии градиента, в которой происходят эти цвета. Здесь 0 означает, что Red происходит в начале градиентной линии, а 1 означает, что Blue происходит в конце строки. Числа должны быть возрастающими и должны находиться в диапазоне от 0 до 1. Если они не в этом диапазоне, они будут скорректированы, чтобы находиться в этом диапазоне.

Эти два значения в массиве могут иметь значение, отличное от 0 и 1. Попробуйте выполните следующее.

new float[] { 0.25f, 0.75f }

Теперь весь первый квартал градиентной линии чистый красный, и последний квартал является чистым синим. Смесь красного и синего ограничена центральной половиной градиентной линии.

Как правило, вы хотите разместить эти значения позиции равным образом от 0 до 1. Если это так, можно просто указать null четвертый аргумент CreateLinearGradient.

Хотя этот градиент определяется между двумя углами прямоугольника 300 пикселей, он не ограничен заполнением прямоугольника. Страница Градиента "Угол в угол" содержит дополнительный код, который реагирует на касания или щелчки мыши на странице. Поле drawBackground переключается между true false и с каждым касанием. Если значение равно true, PaintSurface обработчик использует тот же SKPaint объект для заполнения всего холста, а затем рисует черный прямоугольник, указывающий на меньший прямоугольник:

public class CornerToCornerGradientPage : ContentPage
{
    bool drawBackground;

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

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPaint paint = new SKPaint())
        {
            ···
            if (drawBackground)
            {
                // Draw the gradient on the whole canvas
                canvas.DrawRect(info.Rect, paint);

                // Outline the smaller rectangle
                paint.Shader = null;
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                canvas.DrawRect(rect, paint);
            }
        }
    }
}

Вот что вы увидите после касания экрана:

Полный градиент в углу

Обратите внимание, что градиент повторяется в том же шаблоне за пределами точек, определяющих линию градиента. Это повторение возникает из-за последнего аргумента CreateLinearGradient SKShaderTileMode.Repeat. (Вскоре вы увидите другие варианты.)

Кроме того, обратите внимание, что точки, используемые для указания градиентной линии, не являются уникальными. Линии, которые являются перпендикулярными для линии градиента, имеют тот же цвет, поэтому существует бесконечное количество градиентных линий, которые можно указать для того же эффекта. Например, при заполнении прямоугольника горизонтальным градиентом можно указать верхние и правые верхние и правые угла, а также нижние и нижние правые угла, а также любые две точки, которые даже с этими линиями и параллельны.

Интерактивный эксперимент

Вы можете интерактивно экспериментировать с линейными градиентами с помощью страницы интерактивного линейного градиента . На этой странице используется InteractivePage класс, представленный в статье "Три способа рисования дуги". InteractivePage Обрабатывает TouchEffect события для поддержания коллекции TouchPoint объектов, которые можно перемещать пальцами или мышью.

XAML-файл присоединяет TouchEffect его к родительскому элементу SKCanvasView , а также включает элемент Picker , который позволяет выбрать один из трех элементов SKShaderTileMode перечисления:

<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.InteractiveLinearGradientPage"
                       Title="Interactive Linear 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>

Конструктор в файле программной части создает два TouchPoint объекта для начальных и конечных точек линейного градиента. Обработчик PaintSurface определяет массив из трех цветов (для градиента от красного к синему) и получает текущий SKShaderTileMode из Picker:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    public InteractiveLinearGradientPage ()
    {
        InitializeComponent ();

        touchPoints = new TouchPoint[2];

        for (int i = 0; i < 2; i++)
        {
            touchPoints[i] = new TouchPoint
            {
                Center = new SKPoint(100 + i * 200, 100 + i * 200)
            };
        }

        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.CreateLinearGradient(touchPoints[0].Center,
                                                         touchPoints[1].Center,
                                                         colors,
                                                         null,
                                                         tileMode);
            canvas.DrawRect(info.Rect, paint);
        }
        ···
    }
}

Обработчик PaintSurface создает SKShader объект из всех данных и использует его для цвета всего холста. Для массива значений float задано значение null. В противном случае для равного пространства три цвета вы задали этот параметр массиву со значениями 0, 0,5 и 1.

Основная часть PaintSurface обработчика посвящена отображению нескольких объектов: точек касания как круги контура, градиентная линия и перпендикулярные линии градиентов в точках касания:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // 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);
            }

            // Draw gradient line connecting touchpoints
            canvas.DrawLine(touchPoints[0].Center, touchPoints[1].Center, paint);

            // Draw lines perpendicular to the gradient line
            SKPoint vector = touchPoints[1].Center - touchPoints[0].Center;
            float length = (float)Math.Sqrt(Math.Pow(vector.X, 2) +
                                            Math.Pow(vector.Y, 2));
            vector.X /= length;
            vector.Y /= length;
            SKPoint rotate90 = new SKPoint(-vector.Y, vector.X);
            rotate90.X *= 200;
            rotate90.Y *= 200;

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center - rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center - rotate90,
                            paint);
        }
    }
}

Линия градиента, соединяющая две точки касания, легко нарисовать, но перпендикулярные линии требуют некоторой дополнительной работы. Линия градиента преобразуется в вектор, нормализована, чтобы иметь длину одной единицы, а затем поворачивается на 90 градусов. Затем этот вектор получает длину 200 пикселей. Он используется для рисования четырех линий, простирающихся от точек касания, которые должны быть перпендикулярными к градиентной линии.

Перпендикулярные линии совпадают с началом и концом градиента. Что происходит за пределами этих строк, зависит от параметра перечисления SKShaderTileMode :

Интерактивный линейный градиент

На трех снимках экрана показаны результаты трех разных значений SKShaderTileMode. Снимок экрана SKShaderTileMode.ClampiOS, который просто расширяет цвета на границе градиента. На SKShaderTileMode.Repeat снимке экрана Android показано, как повторяется шаблон градиента. Параметр SKShaderTileMode.Mirror на снимке экрана UWP также повторяет шаблон, но шаблон повторяется каждый раз, что приводит к отсутствии разрывов цвета.

Градиенты на градиентах

Класс SKShader не определяет открытые свойства или методы, кроме Dispose. Объекты SKShader , созданные статическими методами, поэтому неизменяемы. Даже если вы используете один градиент для двух разных объектов, скорее всего, вы захотите немного изменить градиент. Для этого необходимо создать новый SKShader объект.

На странице "Градиентный текст " отображается текст и фигурный план, цвет которых имеет аналогичные градиенты:

Градиентный текст

Единственными различиями в градиентах являются начальные и конечные точки. Градиент, используемый для отображения текста, основан на двух точках в углах ограничивающего прямоугольника для текста. Для фона два пункта основаны на всем холсте. Вот этот код:

public class GradientTextPage : ContentPage
{
    const string TEXT = "GRADIENT";

    public GradientTextPage ()
    {
        Title = "Gradient Text";

        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())
        {
            // Create gradient for background
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                new SKPoint(info.Width, info.Height),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw background
            canvas.DrawRect(info.Rect, paint);

            // Set TextSize to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Shift textBounds by that amount
            textBounds.Offset(xText, yText);

            // Create gradient for text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(textBounds.Left, textBounds.Top),
                                new SKPoint(textBounds.Right, textBounds.Bottom),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

Свойство Shader SKPaint объекта сначала отображает градиент для покрытия фона. Градиентные точки задаются в левом верхнем и правом нижнем углу холста.

Код задает TextSize свойство SKPaint объекта таким образом, чтобы текст отображался на 90 % ширины холста. Границы текста используются для вычисления xText и yText значений, которые DrawText передаются методу в центр текста.

Однако градиентные точки для второго CreateLinearGradient вызова должны ссылаться на верхний левый и правый нижний угол текста относительно холста при отображении. Это достигается путем смены textBounds прямоугольника по тем же xText значениям и yText значениям:

textBounds.Offset(xText, yText);

Теперь верхние и правые нижние угла прямоугольника можно использовать для задания начальной и конечной точек градиента.

Анимация градиента

Существует несколько способов анимации градиента. Одним из способов является анимация начальных и конечных точек. Страница анимации градиентной анимации перемещает две точки вокруг круга, в центре холста. Радиус этого круга составляет половину ширины или высоты холста, в зависимости от того, что меньше. Начальные и конечные точки отличаются друг от друга на этом круге, а градиент переходит от белого к черному с режимом плитки Mirror :

Анимация градиента

Конструктор создает SKCanvasView. OnDisappearing Методы OnAppearing обрабатывают логику анимации:

public class GradientAnimationPage : ContentPage
{
    SKCanvasView canvasView;
    bool isAnimating;
    double angle;
    Stopwatch stopwatch = new Stopwatch();

    public GradientAnimationPage()
    {
        Title = "Gradient Animation";

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

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 3000;
        angle = 2 * Math.PI * (stopwatch.ElapsedMilliseconds % duration) / duration;
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

Метод OnTimerTick вычисляет angle значение, анимированное от 0 до 2π каждые 3 секунды.

Вот один из способов вычисления двух градиентных точек. Именованное SKPoint vector значение вычисляется для расширения от центра холста до точки в радиусе круга. Направление этого вектора основано на синусовых и косинусных значениях угла. Затем вычисляются две противоположные градиентные точки: одна точка вычисляется путем вычитания этого вектора из центра, а другая точка вычисляется путем добавления вектора в центр.

public class GradientAnimationPage : ContentPage
{
    ···
    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())
        {
            SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
            int radius = Math.Min(info.Width, info.Height) / 2;
            SKPoint vector = new SKPoint((float)(radius * Math.Cos(angle)),
                                         (float)(radius * Math.Sin(angle)));

            paint.Shader = SKShader.CreateLinearGradient(
                                center - vector,
                                center + vector,
                                new SKColor[] { SKColors.White, SKColors.Black },
                                null,
                                SKShaderTileMode.Mirror);

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

Для несколько другого подхода требуется меньше кода. Этот подход использует метод перегрузки SKShader.CreateLinearGradient с преобразованием матрицы в качестве последнего аргумента. Этот подход является версией в примере:

public class GradientAnimationPage : ContentPage
{
    ···
    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())
        {
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                info.Width < info.Height ? new SKPoint(info.Width, 0) :
                                                           new SKPoint(0, info.Height),
                                new SKColor[] { SKColors.White, SKColors.Black },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Mirror,
                                SKMatrix.MakeRotation((float)angle, info.Rect.MidX, info.Rect.MidY));

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

Если ширина холста меньше высоты, то для двух точек градиента задано значение (0, 0) и (info.Width0). Преобразование поворота, переданного в качестве последнего аргумента для CreateLinearGradient эффективного поворота этих двух точек вокруг центра экрана.

Обратите внимание, что если угол равен 0, поворот отсутствует, а два градиентных пункта являются верхними и правыми верхними и правыми углами холста. Эти точки не совпадают с градиентными точками, вычисляемыми, как показано в предыдущем CreateLinearGradient вызове. Но эти точки параллельны горизонтальной градиентной линии, которая бисирует центр холста, и они приводят к идентичной градиентности.

Радуга Градиент

Страница "Радуга Градиент " рисует радугу из левого верхнего угла холста в правый нижний угол. Но этот радужный градиент не похож на реальную радугу. Это прямо, а не изогнутые, но он основан на восьми HSL (hue-saturation-luminosity) цвета, которые определяются велоспортом через оттенки значений от 0 до 360:

SKColor[] colors = new SKColor[8];

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

Этот код является частью обработчика, показанного PaintSurface ниже. Обработчик начинается с создания пути, определяющего шестистороннее многоугольник, которое расширяется от верхнего левого угла холста до нижнего правого угла:

public class RainbowGradientPage : ContentPage
{
    public RainbowGradientPage ()
    {
        Title = "Rainbow 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 (SKPath path = new SKPath())
        {
            float rainbowWidth = Math.Min(info.Width, info.Height) / 2f;

            // Create path from upper-left to lower-right corner
            path.MoveTo(0, 0);
            path.LineTo(rainbowWidth / 2, 0);
            path.LineTo(info.Width, info.Height - rainbowWidth / 2);
            path.LineTo(info.Width, info.Height);
            path.LineTo(info.Width - rainbowWidth / 2, info.Height);
            path.LineTo(0, rainbowWidth / 2);
            path.Close();

            using (SKPaint paint = new SKPaint())
            {
                SKColor[] colors = new SKColor[8];

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

                paint.Shader = SKShader.CreateLinearGradient(
                                    new SKPoint(0, rainbowWidth / 2),
                                    new SKPoint(rainbowWidth / 2, 0),
                                    colors,
                                    null,
                                    SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

Две градиентные точки в CreateLinearGradient методе основаны на двух из точек, определяющих этот путь: оба пункта близки к верхнему левому углу. Первый находится на верхнем краю холста, а второй находится на левом крае холста. Ниже приведен результат:

Радуга Градиент неисправный

Это интересный образ, но это не совсем намерение. Проблема заключается в том, что при создании линейного градиента линии константного цвета перпендикулярны к линии градиента. Градиентная линия основана на точках, где фигура касается верхней и левой сторон, и эта линия обычно не перпендикулярна краям фигуры, расширяющейся до нижнего правого угла. Этот подход будет работать только в том случае, если холст был квадратным.

Чтобы создать правильный градиент радуги, линия градиента должна быть перпендикулярной к краю радуги. Это более активное вычисление. Вектор должен быть определен параллельно длинной стороне фигуры. Вектор поворачивается на 90 градусов, чтобы он был перпендикулярным к этой стороне. Затем его длина должна быть шириной фигуры путем умножения на rainbowWidth. Две градиентные точки вычисляются на основе точки на стороне фигуры, и это точка плюс вектор. Ниже приведен код, который отображается на странице "Радуга Градиент " в примере:

public class RainbowGradientPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPath path = new SKPath())
        {
            ···
            using (SKPaint paint = new SKPaint())
            {
                ···
                // Vector on lower-left edge, from top to bottom
                SKPoint edgeVector = new SKPoint(info.Width - rainbowWidth / 2, info.Height) -
                                     new SKPoint(0, rainbowWidth / 2);

                // Rotate 90 degrees counter-clockwise:
                SKPoint gradientVector = new SKPoint(edgeVector.Y, -edgeVector.X);

                // Normalize
                float length = (float)Math.Sqrt(Math.Pow(gradientVector.X, 2) +
                                                Math.Pow(gradientVector.Y, 2));
                gradientVector.X /= length;
                gradientVector.Y /= length;

                // Make it the width of the rainbow
                gradientVector.X *= rainbowWidth;
                gradientVector.Y *= rainbowWidth;

                // Calculate the two points
                SKPoint point1 = new SKPoint(0, rainbowWidth / 2);
                SKPoint point2 = point1 + gradientVector;

                paint.Shader = SKShader.CreateLinearGradient(point1,
                                                             point2,
                                                             colors,
                                                             null,
                                                             SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

Теперь цвета радуги выровнены с рисунком:

Радуга Градиент

Цвета бесконечности

Радужный градиент также используется на странице "Цвета бесконечности ". Эта страница рисует знак бесконечности с помощью объекта пути, описанного в статье "Три типа кривых Bézier". Затем изображение цветом с анимированным градиентом радуги, который постоянно разметается по изображению.

Конструктор создает SKPath объект, описывающий знак бесконечности. После создания пути конструктор также может получить прямоугольные границы пути. Затем он вычисляет вызываемое gradientCycleLengthзначение. Если градиент основан на левом верхнем и правом нижнем углах pathBounds прямоугольника, это gradientCycleLength значение представляет собой общую горизонтальную ширину шаблона градиента:

public class InfinityColorsPage : ContentPage
{
    ···
    SKCanvasView canvasView;

    // Path information
    SKPath infinityPath;
    SKRect pathBounds;
    float gradientCycleLength;

    // Gradient information
    SKColor[] colors = new SKColor[8];
    ···

    public InfinityColorsPage ()
    {
        Title = "Infinity Colors";

        // Create path for infinity sign
        infinityPath = new SKPath();
        infinityPath.MoveTo(0, 0);                                  // Center
        infinityPath.CubicTo(  50,  -50,   95, -100,  150, -100);   // To top of right loop
        infinityPath.CubicTo( 205, -100,  250,  -55,  250,    0);   // To far right of right loop
        infinityPath.CubicTo( 250,   55,  205,  100,  150,  100);   // To bottom of right loop
        infinityPath.CubicTo(  95,  100,   50,   50,    0,    0);   // Back to center  
        infinityPath.CubicTo( -50,  -50,  -95, -100, -150, -100);   // To top of left loop
        infinityPath.CubicTo(-205, -100, -250,  -55, -250,    0);   // To far left of left loop
        infinityPath.CubicTo(-250,   55, -205,  100, -150,  100);   // To bottom of left loop
        infinityPath.CubicTo( -95,  100, - 50,   50,    0,    0);   // Back to center
        infinityPath.Close();

        // Calculate path information
        pathBounds = infinityPath.Bounds;
        gradientCycleLength = pathBounds.Width +
            pathBounds.Height * pathBounds.Height / pathBounds.Width;

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

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

Конструктор также создает colors массив для радуги и SKCanvasView объекта.

Переопределения и OnDisappearing методы OnAppearing выполняют издержки для анимации. Метод OnTimerTick анимирует offset поле от 0 до gradientCycleLength каждых двух секунд:

public class InfinityColorsPage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    float offset;
    Stopwatch stopwatch = new Stopwatch();
    ···

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 2;     // seconds
        double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
        offset = (float)(gradientCycleLength * progress);
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

Наконец, PaintSurface обработчик отображает знак бесконечности. Поскольку путь содержит отрицательные и положительные координаты, окружающие центральную точку (0, 0), Translate преобразование на холсте используется для перемещения его в центр. За преобразованием следует Scale преобразование, которое применяет коэффициент масштабирования, который делает знак бесконечности максимально большим, оставаясь в пределах 95 % ширины и высоты холста.

Обратите внимание, что STROKE_WIDTH константа добавляется к ширине и высоте ограничивающего прямоугольника пути. Путь будет обрисован линией этой ширины, поэтому размер отрисованного бесконечности увеличивается на половину ширины на всех четырех сторонах:

public class InfinityColorsPage : ContentPage
{
    const int STROKE_WIDTH = 50;
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Set transforms to shift path to center and scale to canvas size
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(0.95f *
            Math.Min(info.Width / (pathBounds.Width + STROKE_WIDTH),
                     info.Height / (pathBounds.Height + STROKE_WIDTH)));

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = STROKE_WIDTH;
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(pathBounds.Left, pathBounds.Top),
                                new SKPoint(pathBounds.Right, pathBounds.Bottom),
                                colors,
                                null,
                                SKShaderTileMode.Repeat,
                                SKMatrix.MakeTranslation(offset, 0));

            canvas.DrawPath(infinityPath, paint);
        }
    }
}

Посмотрите на точки, переданные в качестве первых двух аргументов SKShader.CreateLinearGradient. Эти точки основаны на исходном пути, ограничивающем прямоугольник. Первая точка — (–250, –100), а вторая — (250, 100). Внутренние к SkiaSharp эти точки подвергаются текущему преобразованию холста, чтобы они правильно соответствовали отображаемой бесконечности знака.

Без последнего аргумента CreateLinearGradient, вы увидите радужный градиент, который простирается от верхнего левого левого знака бесконечности до нижнего правого. (Фактически, градиент расширяется от верхнего левого угла до нижнего правого угла ограничивающего прямоугольника. Отрисованный знак бесконечности больше ограничивающего прямоугольника на половину STROKE_WIDTH значения со всех сторон. Поскольку градиент является красным как в начале, так и в конце, и градиент создается с SKShaderTileMode.Repeat, разница не заметно.)

С этим последним аргументом CreateLinearGradient, шаблон градиента непрерывно перемещается по изображению:

Цвета бесконечности

Прозрачность и градиенты

Цвета, которые способствуют градиенту, могут включать прозрачность. Вместо градиента, который исчезает из одного цвета в другой, градиент может исчезать от цвета до прозрачного.

Этот метод можно использовать для некоторых интересных эффектов. Один из классических примеров показывает графический объект со своим отражением:

Рефлексия ion Gradient

Текст, который перевернут вниз, цветом является градиент, который равен 50 % прозрачным в верхней части, чтобы полностью прозрачным в нижней части. Эти уровни прозрачности связаны с альфа-значениями 0x80 и 0.

Обработчик PaintSurface на странице Рефлексия ion Gradient масштабирует размер текста до 90 % ширины холста. Затем он вычисляет xText и yText значения для размещения текста в горизонтальном центре, но сидит на базовом уровне, соответствующем вертикальному центру страницы:

public class ReflectionGradientPage : ContentPage
{
    const string TEXT = "Reflection";

    public ReflectionGradientPage ()
    {
        Title = "Reflection 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())
        {
            // Set text color to blue
            paint.Color = SKColors.Blue;

            // Set text size to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to position text above center
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2;

            // Draw unreflected text
            canvas.DrawText(TEXT, xText, yText, paint);

            // Shift textBounds to match displayed text
            textBounds.Offset(xText, yText);

            // Use those offsets to create a gradient for the reflected text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, textBounds.Top),
                                new SKPoint(0, textBounds.Bottom),
                                new SKColor[] { paint.Color.WithAlpha(0),
                                                paint.Color.WithAlpha(0x80) },
                                null,
                                SKShaderTileMode.Clamp);

            // Scale the canvas to flip upside-down around the vertical center
            canvas.Scale(1, -1, 0, yText);

            // Draw reflected text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

Эти xText и yText значения являются теми же значениями, которые используются для отображения отраженного текста в вызове в DrawText нижней части обработчика PaintSurface . Однако перед этим кодом вы увидите вызов Scale метода SKCanvas. Этот Scale метод масштабируется горизонтально на 1 (что ничего не делает), но по вертикали на –1, что эффективно переворачивает все вверх на голову. Центр поворота устанавливается в точку (0, yTextгде yText вертикальный центр холста, первоначально вычисляется как info.Height разделенный на 2).

Помните, что Skia использует градиент для цвета графических объектов до преобразования холста. После рисования неотражаемого текста прямоугольник перемещается таким образом, textBounds чтобы он соответствовал отображаемого текста:

textBounds.Offset(xText, yText);

Вызов CreateLinearGradient определяет градиент из верхней части прямоугольника внизу. Градиент от полностью прозрачного синего (paint.Color.WithAlpha(0)) до 50 % прозрачного синего (paint.Color.WithAlpha(0x80)). Преобразование холста переворачивает текст вверх и вниз, поэтому 50% прозрачный синий начинается на базовом уровне и становится прозрачным в верхней части текста.