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


Эффекты пути в SkiaSharp

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

Эффект пути — это экземпляр SKPathEffect класса, который создается с одним из восьми статических методов создания, определенных классом. Затем SKPathEffect объект присваивается PathEffect свойству SKPaint объекта для различных интересных эффектов, например, построив линию с небольшим реплика заданным путем:

Пример связанной цепочки

Эффекты пути позволяют:

  • Росчерк линии с точками и дефисами
  • Росчерк линии с любым заполненным путем
  • Заполнение области линиями люка
  • Заполнение области плиткой
  • Сделать острые угловые округленные
  • Добавление случайного "jitter" в линии и кривые

Кроме того, можно объединить два или более эффектов пути.

В этой статье также показано, как использовать GetFillPath метод SKPaint преобразования одного пути в другой путем применения свойств SKPaint, включая StrokeWidth и PathEffect. Это приводит к некоторым интересным методам, таким как получение пути, который является контуром другого пути. GetFillPath также полезно в связи с эффектами пути.

Точки и тире

Использование PathEffect.CreateDash метода описано в статье Dots и Dashes. Первым аргументом метода является массив, содержащий даже количество двух или более значений, чередующееся между длинами дефисов и длиной пробелов между дефисами:

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

Эти значения не относятся к ширине штриха. Например, если ширина штриха равна 10, и вы хотите, чтобы линия, состоящая из квадратных дефисов и квадратных пробелов, задайте intervals для массива значение { 10, 10 }. Аргумент phase указывает, где начинается строка в шаблоне дефиса. В этом примере, если вы хотите, чтобы строка начиналось с квадратного разрыва, задайте phase значение 10.

Концы дефисов влияют на StrokeCap свойство SKPaint. Для ширины широких штрихов это свойство SKStrokeCap.Round очень часто задает для округления концы дефисов. В этом случае значения в intervals массиве не включают дополнительную длину, полученную от округления. Это означает, что круговая точка требует указания ширины нуля. Для ширины штриха 10 для создания линии с круговыми точками и пробелами между точками одного диаметра используйте intervals массив {0, 20 }.

Страница анимированного текста похожа на страницу "Контурированный текст", описанную в статье "Интеграция текста и графики", в котором он отображает контурные текстовые символы, задав Style свойству SKPaint объекта значение SKPaintStyle.Stroke. Кроме того, анимированный текст используется SKPathEffect.CreateDash для предоставления этого контура пунктирного внешнего вида, а программа также анимирует phase аргумент SKPathEffect.CreateDash метода, чтобы точки, казалось, перемещались по текстовым символам. Вот страница в альбомном режиме:

Тройной снимок экрана: страница

Класс AnimatedDottedTextPage начинается с определения некоторых констант, а также переопределяет OnAppearing методы анимации:OnDisappearing

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

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

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

Обработчик PaintSurface начинается с создания SKPaint объекта для отображения текста. Свойство TextSize настраивается на основе ширины экрана:

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

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.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;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

К концу метода SKPathEffect.CreateDash метод вызывается с помощью dashArray поля и анимированного phase значения. Экземпляр SKPathEffect присваивается PathEffect свойству SKPaint объекта для отображения текста.

Кроме того, можно задать SKPathEffect объект SKPaint объекту перед измерением текста и его центрированием на странице. Однако в этом случае анимированные точки и дефисы вызывают некоторые вариации размера отрисованного текста, а текст, как правило, вибрирует немного. (Попробуйте!)

Вы также заметите, что, как анимированные точки вокруг текстовых символов, есть определенная точка в каждой закрытой кривой, где точки, кажется, всплывают и вне существования. Именно здесь начинается и заканчивается путь, определяющий структуру символов. Если длина пути не является неотъемлемой частью длины шаблона дефиса (в данном случае 20 пикселей), то только часть этого шаблона может соответствовать в конце пути.

Можно настроить длину тире, чтобы соответствовать длине пути, но для этого требуется определить длину пути, метод, описанный в статье "Сведения о пути" и "Перечисление".

Программа Dot / Dash Morph анимирует сам шаблон тире, чтобы тире, казалось, разделялись на точки, которые объединяются для формирования дефисов снова:

Тройной снимок экрана страницы Dot Dash Morph

Класс DotDashMorphPage переопределяет OnAppearing методы и OnDisappearing методы так же, как и предыдущая программа, но класс определяет SKPaint объект как поле:

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

Обработчик PaintSurface создает многоточие на основе размера страницы и выполняет длинный раздел кода, который задает dashArray и phase переменные. По мере того как анимированные переменные t варьируются от 0 до 1, if блоки разбиваются на четыре четверти, tsub а в каждом из этих кварталов также диапазон от 0 до 1. В самом конце программа создает SKPathEffect и задает его SKPaint объекту для рисования.

Путь к пути

Метод GetFillPathSKPaint превращает один путь в другой на основе параметров объекта SKPaint . Чтобы узнать, как это работает, замените canvas.DrawPath вызов в предыдущей программе следующим кодом:

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

В этом новом коде GetFillPath вызов преобразует ellipsePath (который просто овальный) в newPath, который затем отображается с newPaintпомощью . Объект newPaint создается со всеми параметрами свойств по умолчанию, за исключением того, что Style свойство задано на основе логического возвращаемого значения GetFillPath.

Визуальные элементы идентичны, за исключением цвета, в котором задано ellipsePaint значение, но не newPaint. Вместо простого многоточия, определенного в ellipsePath, newPath содержит многочисленные контуры пути, определяющие ряд точек и дефисов. Это результат применения различных свойств ellipsePaint (в частности, StrokeWidth, StrokeCapи PathEffect) для ellipsePath и размещения результирующий путь newPath. Метод GetFillPath возвращает логическое значение, указывающее, должен ли быть заполнен путь назначения. В этом примере возвращаемое значение предназначено true для заполнения пути.

Попробуйте изменить Style параметр newPaintSKPaintStyle.Stroke на, и вы увидите контуры отдельных путей, описанные с помощью линии одно пиксельной ширины.

Построка с путем

Метод SKPathEffect.Create1DPath концептуально похож на SKPathEffect.CreateDash то, что вы указываете путь, а не шаблон дефисов и пробелов. Этот путь реплика несколько раз для росчерка линии или кривой.

Синтаксис:

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

Как правило, путь, который вы передаете Create1DPath , будет небольшим и центрирован вокруг точки (0, 0). Параметр advance указывает расстояние между центрами пути, так как путь реплика в строке. Обычно этот аргумент устанавливается на приблизительную ширину пути. Аргумент phase играет ту же роль, что и в методе CreateDash .

У SKPath1DPathEffectStyle него три члена:

  • Translate
  • Rotate
  • Morph

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

На странице эффектов 1D-пути показаны три варианта. Файл OneDimensionalPathEffectPage.xaml определяет средство выбора, содержащее три элемента, соответствующие трем элементам перечисления:

<?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.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

Файл кода OneDimensionalPathEffectPage.xaml.cs определяет три SKPathEffect объекта в виде полей. Все они создаются с использованием SKPathEffect.Create1DPathSKPath объектов, созданных с помощью SKPath.ParseSvgPathData. Первое — это простое поле, второй — ромбовая форма, а третья — прямоугольник. Они используются для демонстрации трех стилей эффектов:

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

    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())
        {
            path.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

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

Обработчик PaintSurface создает кривую Bézier, которая циклит вокруг себя, и обращается к средство выбора, чтобы определить, какой PathEffect должен использоваться для почерка. Три варианта — Translateи RotateMorph — отображаются слева направо:

Тройной снимок экрана страницы эффектов 1D-пути

Путь, указанный в методе SKPathEffect.Create1DPath , всегда заполняется. Путь, указанный в методе DrawPath , всегда обводится, если SKPaint объект имеет его PathEffect свойство, равное эффекту 1D-пути. Обратите внимание, что pathPaint объект не Style имеет параметра, который обычно по умолчанию используется Fill, но путь почеркается независимо от него.

Поле, используемое Translate в примере, равно 20 пикселям, и advance аргумент имеет значение 24. Это различие приводит к разрыву между полями, когда линия примерно горизонтальная или вертикальная, но прямоугольники перекрываются немного, когда линия диагонали, потому что диагонали прямоугольника составляет 28,3 пикселя.

Форма бриллианта Rotate в примере также составляет 20 пикселей. Задано advance значение 20, чтобы точки продолжали касаться, как алмаз поворачивается вместе с кривизной линии.

Фигура прямоугольника в Morph примере имеет ширину 50 пикселей с параметром advance 55, чтобы сделать небольшой разрыв между прямоугольниками, так как они согнуты вокруг кривой Bézier.

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

Тройной снимок экрана страницы связанной цепочки

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

Цепочка или кабель равномерного распределения веса висит в виде катенарии. Арка, построенная в виде инвертированного катенарного преимущества от равного распределения давления от веса арки. Catenary имеет, казалось бы, простое математическое описание:

y = a · cosh(x / a)

Кош является гиперболической функцией косинуса. Для x равно 0 cosh равно нулю и y равно нулю. Это центр катенарии. Как и функция косинуса, cosh, как говорят, даже, что означает, что cosh(-x) равно cosh(x), и значения увеличиваются для повышения положительных или отрицательных аргументов. Эти значения описывают кривые, которые образуют стороны катенария.

Поиск правильного значения для соответствия катенарии измерениям страницы телефона не является прямым вычислением. Если w и h являются шириной и высотой прямоугольника, оптимальным значением является следующее уравнение:

cosh(w / 2 / a) = 1 + h / a

Следующий метод в LinkedChainPage классе включает в себя равенство, ссылаясь на два выражения слева и справа от знака равенства как left и right. Для небольших значений leftобъекта больше, left чем right; для больших значений объекта меньше.right Цикл while сужается на оптимальном значении:

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

Объект SKPath для ссылок создается в конструкторе класса, а результирующий SKPathEffect объект затем присваивается PathEffect свойству SKPaint объекта, хранящегося в виде поля:

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

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

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

Основной задачей обработчика PaintSurface является создание пути для самой катенарии. После определения оптимального значения и хранения его в optA переменной также необходимо вычислить смещение в верхней части окна. Затем он может накапливать коллекцию значений SKPoint для катенарии, превращать ее в путь и рисовать путь с ранее созданным SKPaint объектом:

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

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

Эта программа определяет путь, используемый Create1DPath для его точки (0, 0) в центре. Это кажется разумным, потому что точка пути (0, 0) выравнивается с линией или кривой, что это украшение. Однако для некоторых специальных эффектов можно использовать нецентрированную точку (0, 0).

Страница конвейерного пояса создает путь, похожий на продолговатый конвейерный пояс с изогнутым верхним и нижним размером окна. Этот путь перечеркается с простым SKPaint объектом 20 пикселей шириной и цветным серым цветом, а затем перечеркнулся еще раз с другим SKPaint объектом с объектом, SKPathEffect ссылающимся на путь, напоминающий небольшой контейнер:

Тройной снимок экрана страницы конвейерного пояса

Точка (0, 0) пути к контейнеру является дескриптором, поэтому, когда phase аргумент анимирован, контейнеры, кажется, вращаются вокруг конвейерного пояса, возможно, черпая воду в нижней части и сбрасывая его в верхней части.

Класс ConveyorBeltPage реализует анимацию с переопределениями OnAppearing методов и OnDisappearing методов. Путь к контейнеру определяется в конструкторе страницы:

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

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

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

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

Обработчик PaintSurface начинается с определения пути для самого конвейерного пояса. Это просто пара линий и пара полукольцев, которые рисуются с 20-пиксельной темно-серой линией:

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

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

Логика рисования конвейерного пояса не работает в альбомном режиме.

Контейнеры должны быть разделены около 200 пикселей на ленте конвейера. Однако конвейерный пояс, вероятно, не является кратным 200 пикселей длиной, что означает, что как phase аргумент SKPathEffect.Create1DPath анимирован, контейнеры будут всплыть в и вне существования.

По этой причине программа сначала вычисляет значение с именем length , которое является длиной конвейерного пояса. Поскольку конвейерная лента состоит из прямых линий и полукольцев, это простое вычисление. Затем число сегментов вычисляется путем деления length на 200. Это округляется до ближайшего целого числа, и это число затем делится на length. Результатом является интервал для целого числа контейнеров. Аргумент phase является лишь частью этого.

Путь к пути снова

В нижней части обработчика DrawSurface в конвейерном поясе закомментируйте canvas.DrawPath вызов и замените его следующим кодом:

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Как и в предыдущем примере GetFillPath, вы увидите, что результаты одинаковы, кроме цвета. После выполнения GetFillPathnewPath объект содержит несколько копий пути к контейнеру, каждый расположен в том же месте, что анимация размещает их во время вызова.

Вылупление области

Метод SKPathEffect.Create2DLines заполняет область параллельными линиями, часто называемыми линиями хэтча. Метод имеет следующий синтаксис:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

Аргумент width задает ширину штриха линий штриха. Параметр matrix — это сочетание масштабирования и необязательного поворота. Коэффициент масштабирования указывает на увеличение пикселя, которое Skia использует для пространства линий люка. Разделение между строками является коэффициентом масштабирования минус width аргумент. Если коэффициент масштабирования меньше или равен width значению, между линиями люка не будет места, а область будет заполнена. Укажите то же значение горизонтального и вертикального масштабирования.

По умолчанию линии хэтча являются горизонтальными. matrix Если параметр содержит поворот, линии люка поворачиваются по часовой стрелке.

Страница заливки хэтча демонстрирует этот эффект пути. Класс HatchFillPage определяет три эффекта пути в виде полей, первый для горизонтальных линий хэтча с шириной 3 пикселя с коэффициентом масштабирования, указывающим, что они расположены на 6 пикселей друг от друга. Таким образом, разделение между линиями составляет три пикселя. Второй эффект пути предназначен для вертикальных линий хэтча с шириной 24 пикселей, разделенных на 24 пикселя (поэтому разделение составляет 18 пикселей), а третий — для линии диагонального хэтча 12 пикселей с шириной 36 пикселей.

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

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

Обработчик PaintSurface использует эти три эффекта пути с тремя разными цветами в сочетании с fillPaint заливкой округленного прямоугольника, чтобы соответствовать странице. Набор Style свойств fillPaint игнорируется; если SKPaint объект включает эффект пути, созданный из SKPathEffect.Create2DLine, область заполняется независимо от следующего:

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

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

Если внимательно посмотреть на результаты, вы увидите, что красные и синие линии хэтча не ограничиваются точно округленным прямоугольником. (Это, по-видимому, характеристика базового кода Skia.) Если это неудовлетворительно, альтернативный подход показан для линии диагонали хэтча в зеленом цвете: округленный прямоугольник используется в качестве вырезки пути и линии люка рисуются на всей странице.

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

Тройной снимок экрана страницы заливки хэтча

Экран Android не выглядит так: масштабирование снимка экрана привело к консолидации тонких красных линий и тонких пространств, казалось бы, более широких красных линий и более широких пространств.

Заполнение путем

Это SKPathEffect.Create2DPath позволяет заполнить область путем, реплика реплика горизонтально и вертикально, в эффекте навески области:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

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

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

Страница заливки плитки пути демонстрирует этот эффект пути. Путь, используемый для размещения области, определяется как поле в PathTileFillPage классе. Горизонтальная и вертикальная координаты варьируются от –40 до 40, что означает, что этот путь составляет 80 пикселей:

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    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.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

В обработчике PaintSurfaceSKPathEffect.Create2DPath вызовы задают горизонтальное и вертикальное интервалы 64, чтобы привести к перекрытию 80-пикселей квадратных плиток. К счастью, путь напоминает кусок головоломки, сетка приятно с присоединяющими плитками:

Тройной снимок экрана: страница заливки плитки пути

Масштабирование с исходного снимка экрана приводит к некоторому искажению, особенно на экране Android.

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

Попробуйте задать Style для свойства SKPaint объекта Strokeзначение, и вы увидите отдельные плитки, описанные вместо заполнения.

Кроме того, можно заполнить область плиткой растрового изображения, как показано в статье SkiaSharp bitmap tiling.

Округление острых углов

Программа округленного гептагона, представленная в трех способах рисования статьи Arc, использовала тангенсную дугу для кривых точек семистороннего рисунка. На странице "Другой округленный гептагон " показан гораздо более простой подход, который использует эффект пути, созданный из SKPathEffect.CreateCorner метода:

public static SKPathEffect CreateCorner (Single radius)

Хотя один аргумент называется radius, необходимо задать для него половину требуемого радиуса угла. (Это характеристика базового кода Skia.)

Ниже приведен PaintSurface обработчик класса AnotherRoundedHeptagonPage :

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

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = new SKPoint[numVertices];
    double vertexAngle = -0.5f * Math.PI;       // straight up

    // Coordinates of the vertices of the polygon
    for (int vertex = 0; vertex < numVertices; vertex++)
    {
        vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                       radius * (float)Math.Sin(vertexAngle));
        vertexAngle += 2 * Math.PI / numVertices;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // Render the path in the center of the screen
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = 10;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

Этот эффект можно использовать с поглаживание или заполнение на Style основе свойства SKPaint объекта. Здесь выполняется:

Тройной снимок экрана страницы

Вы увидите, что этот округленный гептагон идентичен предыдущей программе. Если вам нужно более убедительным, что радиус угла действительно 100, а не 50, указанный в SKPathEffect.CreateCorner вызове, можно раскомментируйте окончательный оператор в программе и увидеть 100-радиусный круг, наложенный на угол.

Случайный jitter

Иногда безупречные прямые линии компьютерной графики не совсем то, что вы хотите, и немного случайности требуется. В этом случае вам потребуется попробовать SKPathEffect.CreateDiscrete этот метод:

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

Этот эффект пути можно использовать для поглаживания или заполнения. Линии разделены на подключенные сегменты — приблизительную длину, указанную segLength в разных направлениях, и расширяются в разных направлениях. Степень отклонения от исходной строки задается deviation.

Последний аргумент — это начальное значение, используемое для создания псевдослучайной последовательности, используемой для эффекта. Эффект жима будет выглядеть немного иначе для разных семян. Аргумент имеет значение по умолчанию нулю, что означает, что эффект совпадает при выполнении программы. Если при повторном переопределении экрана требуется другое jitter, можно задать начальное значение для Millisecond свойства DataTime.Now значения (например.

Страница "Эксперимент Jitter" позволяет экспериментировать с различными значениями при построке прямоугольника:

Тройной снимок экрана страницы JitterExperiment

Программа проста. Файл JitterExperimentPage.xaml создает экземпляры двух Slider элементов и :SKCanvasView

<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.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Обработчик PaintSurface в файле кода JitterExperimentPage.xaml.cs вызывается всякий Slider раз, когда изменяется значение. SKPathEffect.CreateDiscrete Вызывается с помощью двух Slider значений и используется для росчерка прямоугольника:

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

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

Этот эффект также можно использовать для заполнения, в этом случае контур заполненной области подвергается этим случайным отклонениям. Страница Jitter Text демонстрирует использование этого эффекта пути для отображения текста. Большая часть кода в PaintSurface обработчике JitterTextPage класса посвящена размеру и центризации текста:

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

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.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;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Здесь он выполняется в альбомном режиме:

Тройной снимок экрана страницы JitterText

Определение пути

Вы уже видели два маленьких GetFillPath примера метода SKPaint, который существует две версии:

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

Требуются только первые два аргумента. Метод обращается к пути, на который src ссылается аргумент, изменяет данные пути на основе свойств штриха в SKPaint объекте (включая PathEffect свойство), а затем записывает результаты в dst путь. Параметр resScale позволяет уменьшить точность для создания меньшего пути назначения, а cullRect аргумент может исключить контуры за пределами прямоугольника.

Один из основных способов использования этого метода не включает эффекты пути вообще: если SKPaint объект имеет значение свойства StyleSKPaintStyle.Stroke, и не имеет его PathEffect набора, то GetFillPath создает путь, представляющий контур исходного пути, как если бы он был обрисовывается свойствами краски.

Например, если src путь является простым кругом радиуса 500, а SKPaint объект задает ширину штриха 100, dst то путь становится двумя концентрическими кругами, один с радиусом 450, а другой с радиусом 550. Метод вызывается GetFillPath , так как заполнение этого dst пути совпадает с построкой src пути. Но вы также можете обвести dst путь, чтобы увидеть контуры пути.

Касание , чтобы указать путь , демонстрирует это. Он SKCanvasView создается в файле TapToOutlineThePathPage.xaml.TapGestureRecognizer Файл кода TapToOutlineThePathPage.xaml.cs определяет три SKPaint объекта в виде полей, два для построек с шириной штриха 100 и 20, а третий — для заполнения:

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

Если экран не был касался, PaintSurface обработчик использует blueFill объекты и redThickStroke красит объекты для отрисовки кругового пути:

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

Круг заполнен и росчерк по мере ожидания:

Тройной снимок экрана: обычный касание, чтобы указать страницу пути

При касании экрана задано trueзначение , outlineThePath а PaintSurface обработчик создает свежий SKPath объект и использует его в качестве пути назначения в вызове GetFillPathredThickStroke объекта рисования. Затем этот путь назначения заполняется и перечеркается redThinStroke, что приводит к следующему:

Тройной снимок экрана, на котором показана страница

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

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

Например, в примере "Связанная цепочка " ссылки были определены с серией четырех дуг, каждая пара из которых была основана на двух радиях, чтобы указать область пути, которую необходимо заполнить. Можно заменить код в LinkedChainPage классе немного иначе.

Во-первых, необходимо переопределить константу linkRadius :

const float linkRadius = 27.5f;
const float linkThickness = 5;

Теперь linkPath это всего два дуги на основе этого одного радиуса, с нужными углами начала и углами сверток:

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

Затем outlinePath объект является получателем контура linkPath , когда он обрисовывается со свойствами, указанными в strokePaint.

Другой пример, использующий этот метод, приближается к следующему пути, используемому в методе.

Объединение эффектов пути

Два последних SKPathEffect метода статического создания:SKPathEffect.CreateSumSKPathEffect.CreateCompose

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Оба этих метода объединяют два эффекта пути для создания составного эффекта пути. Метод CreateSum создает эффект пути, аналогичный двум эффектам пути, применяемым отдельно, при этом CreateCompose применяется один эффект пути (the inner), а затем применяется outer к этому.

Вы уже видели, как метод SKPaint может преобразовать один путь в другой путь на SKPaint основе свойств (включаяPathEffect) так что он не должен быть слишком таинственным, как SKPaintGetFillPath объект может выполнять эту операцию дважды с двумя эффектами пути, указанными в CreateSum или CreateCompose методах.

Одним из очевидных способов CreateSum является определение SKPaint объекта, заполняющего путь одним эффектом пути, и штрихи пути с другим эффектом пути. Это демонстрируется в примере "Кошки в кадре", в котором отображается массив кошек в кадре с скалятыми краями:

Тройной снимок экрана: страница

Класс CatsInFramePage начинается с определения нескольких полей. Вы можете распознать первое поле из класса из PathDataCatPageстатьи данных пути SVG. Второй путь основан на линии и дуге для шаблона скалы кадра:

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    SKPath catPath = SKPath.ParseSvgPathData(
        "M 160 140 L 150 50 220 103" +              // Left ear
        "M 320 140 L 330 50 260 103" +              // Right ear
        "M 215 230 L 40 200" +                      // Left whiskers
        "M 215 240 L 40 240" +
        "M 215 250 L 40 280" +
        "M 265 230 L 440 200" +                     // Right whiskers
        "M 265 240 L 440 240" +
        "M 265 250 L 440 280" +
        "M 240 100" +                               // Head
        "A 100 100 0 0 1 240 300" +
        "A 100 100 0 0 1 240 100 Z" +
        "M 180 170" +                               // Left eye
        "A 40 40 0 0 1 220 170" +
        "A 40 40 0 0 1 180 170 Z" +
        "M 300 170" +                               // Right eye
        "A 40 40 0 0 1 260 170" +
        "A 40 40 0 0 1 300 170 Z");

    SKPaint catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

Его catPath можно использовать в методе SKPathEffect.Create2DPath , если SKPaint для свойства объекта Style задано значение Stroke. Тем не менее, если используется catPath непосредственно в этой программе, то весь голова кота будет заполнена, и виски даже не будут видны. (Попробуйте!) Необходимо получить структуру этого пути и использовать ее в методе SKPathEffect.Create2DPath .

Конструктор выполняет это задание. Сначала он применяет два преобразования для catPath перемещения точки (0, 0) в центр и уменьшения его размера. GetFillPath получает все контуры контуров outlinedCatPathи этот объект используется в вызове SKPathEffect.Create2DPath . Коэффициенты SKMatrix масштабирования в значении немного больше, чем горизонтальный и вертикальный размер кошки, чтобы обеспечить небольшой буфер между плитками, в то время как факторы перевода были получены несколько эмпирически, чтобы полная кошка видна в верхнем левом углу кадра:

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

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

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

Затем конструктор вызывает SKPathEffect.Create1DPath скалятый кадр. Обратите внимание, что ширина пути составляет 100 пикселей, но расстояние составляет 75 пикселей, чтобы реплика путь перекрывался вокруг кадра. Окончательный оператор конструктора вызывает SKPathEffect.CreateSum объединение двух эффектов пути и задания результата объекту SKPaint .

Все это позволяет PaintSurface обработчику быть довольно простым. Он должен определить прямоугольник и нарисовать его с помощью framePaint:

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

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

Алгоритмы эффектов пути всегда вызывают весь путь, используемый для отображения поглаживания или заполнения, что может привести к отображению некоторых визуальных элементов за пределами прямоугольника. Вызов ClipRect до DrawRect вызова позволяет визуальным элементам быть значительно более чистым. (Попробуйте его без вырезки!)

Обычно используется для SKPathEffect.CreateCompose добавления некоторых jitter в другой эффект пути. Вы, конечно, можете поэкспериментировать самостоятельно, но вот несколько другой пример:

Тиреные линии хэтч заполняет многоточие линиями люка, которые дефисируются. Большая часть работы в классе выполняется прямо в DashedHatchLinesPage определениях полей. Эти поля определяют эффект дефиса и эффект хэтча. Они определяются так, static как они затем ссылаются в вызове SKPathEffect.CreateCompose в определении SKPaint :

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Обработчик PaintSurface должен содержать только стандартные затраты, а также один вызов:DrawOval

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

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

Как вы уже обнаружили, линии хэтча точно не ограничены интерьером области, и в этом примере они всегда начинаются слева с целым тире:

Тройной снимок экрана страницы

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