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


Сведения о пути и перечисление

Получение сведений о путях и перечислении содержимого

Класс SKPath определяет несколько свойств и методов, позволяющих получить сведения о пути. TightBounds Свойства Bounds (и связанные методы) получают метрики пути. Этот Contains метод позволяет определить, находится ли определенная точка в пути.

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

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

Текст, завернутый в полушарие

Получение длины пути

В статье "Пути и текст " вы узнали, как использовать DrawTextOnPath метод для рисования текстовой строки, базовые показатели которой соответствуют курсу пути. Но что делать, если вы хотите размер текста таким образом, чтобы он точно соответствовал пути? Рисование текста вокруг круга легко, так как окружность круга проста для вычисления. Но окружность многоточия или длина кривой Bézier не так проста.

Класс SKPathMeasure может помочь. Конструктор принимает SKPath аргумент, и Length свойство показывает его длину.

Этот класс демонстрируется в примере длины пути, основанном на странице "Кривая Bezier". Файл PathLengthPage.xaml является производным от InteractivePage и включает сенсорный интерфейс:

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
                       Title="Path Length">
    <Grid BackgroundColor="White">
        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction" />
        </Grid.Effects>
    </Grid>
</local:InteractivePage>

Файл PathLengthPage.xaml.cs кода позволяет перемещать четыре точки касания для определения конечных точек и контрольных точек кубической кривой Bézier. Три поля определяют текстовую строку, SKPaint объект и вычисляемую ширину текста:

public partial class PathLengthPage : InteractivePage
{
    const string text = "Compute length of path";

    static SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Black,
        TextSize = 10,
    };

    static readonly float baseTextWidth = textPaint.MeasureText(text);
    ...
}

Поле baseTextWidth — это ширина текста на TextSize основе параметра 10.

Обработчик PaintSurface рисует кривую Bézier, а затем размер текста, который будет соответствовать его полной длине:

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

    canvas.Clear();

    // Draw path with cubic Bezier curve
    using (SKPath path = new SKPath())
    {
        path.MoveTo(touchPoints[0].Center);
        path.CubicTo(touchPoints[1].Center,
                     touchPoints[2].Center,
                     touchPoints[3].Center);

        canvas.DrawPath(path, strokePaint);

        // Get path length
        SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);

        // Find new text size
        textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;

        // Draw text on path
        canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
    }
    ...
}

Свойство Length только что созданного SKPathMeasure объекта получает длину пути. Длина пути делится baseTextWidth на значение (это ширина текста на основе размера текста 10), а затем умножается на базовый размер текста 10. Результатом является новый размер текста для отображения текста вдоль этого пути:

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

По мере того как кривая Bézier становится длиннее или короче, можно увидеть изменение размера текста.

Обход пути

SKPathMeasure может сделать больше, чем просто измерять длину пути. Для любого значения от нуля до длины SKPathMeasure пути объект может получить позицию по пути и тангенс к кривой пути в этой точке. Тангенс доступен в виде вектора в виде объекта или в виде поворота SKPoint , инкапсулированного в SKMatrix объекте. Ниже приведены методы SKPathMeasure получения этой информации различными и гибкими способами:

Boolean GetPosition (Single distance, out SKPoint position)

Boolean GetTangent (Single distance, out SKPoint tangent)

Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)

Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)

Члены перечисления SKPathMeasureMatrixFlags :

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

Страница Unicycle Half-Pipe анимирует фигуру палки на юницикле, который, кажется, ездить назад и вперед вдоль кубической кривой Bézier:

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

Объект SKPaint , используемый для поглаживания как половины канала, так и юницикла, определяется как поле в UnicycleHalfPipePage классе. Также определен объект для юницикла SKPath :

public class UnicycleHalfPipePage : ContentPage
{
    ...
    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };

    SKPath unicyclePath = SKPath.ParseSvgPathData(
        "M 0 0" +
        "A 25 25 0 0 0 0 -50" +
        "A 25 25 0 0 0 0 0 Z" +
        "M 0 -25 L 0 -100" +
        "A 15 15 0 0 0 0 -130" +
        "A 15 15 0 0 0 0 -100 Z" +
        "M -25 -85 L 25 -85");
    ...
}

Класс содержит стандартные переопределения и OnDisappearing методы OnAppearing анимации. Обработчик PaintSurface создает путь для половинного канала, а затем рисует его. Затем SKPathMeasure объект создается на основе этого пути:

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

        canvas.Clear();

        using (SKPath pipePath = new SKPath())
        {
            pipePath.MoveTo(50, 50);
            pipePath.CubicTo(0, 1.25f * info.Height,
                             info.Width - 0, 1.25f * info.Height,
                             info.Width - 50, 50);

            canvas.DrawPath(pipePath, strokePaint);

            using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
            {
                float length = pathMeasure.Length;

                // Animate t from 0 to 1 every three seconds
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 5 / 5);

                // t from 0 to 1 to 0 but slower at beginning and end
                t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);

                SKMatrix matrix;
                pathMeasure.GetMatrix(t * length, out matrix,
                                      SKPathMeasureMatrixFlags.GetPositionAndTangent);

                canvas.SetMatrix(matrix);
                canvas.DrawPath(unicyclePath, strokePaint);
            }
        }
    }
}

Обработчик PaintSurface вычисляет значение t от 0 до 1 каждые пять секунд. Затем функция используется Math.Cos для преобразования этого значения в диапазон t от 0 до 1 и обратно в 0, где 0 соответствует юнициклу в начале слева, а 1 соответствует юнициклу в правом верхнем углу. Функция косинуса приводит к замедлению скорости в верхней части трубы и самой быстрой в нижней части.

Обратите внимание, что это значение t должно умножаться на длину пути для первого аргумента GetMatrix. Затем матрица применяется к SKCanvas объекту для рисования пути юницикла.

Перечисление пути

Два внедренных класса SKPath позволяют перечислять содержимое пути. Эти классы и SKPath.IteratorSKPath.RawIterator. Два класса очень похожи, но SKPath.Iterator могут устранять элементы в пути с нулевой длиной или близко к нулевой длине. Используется RawIterator в приведенном ниже примере.

Объект типа SKPath.RawIterator можно получить, вызвав CreateRawIterator метод SKPath. Перечисление по пути выполняется путем многократного Next вызова метода. Передайте в него массив из четырех SKPoint значений:

SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);

Метод Next возвращает элемент SKPathVerb типа перечисления. Эти значения указывают определенную команду рисования в пути. Количество допустимых точек, вставленных в массив, зависит от этой команды:

  • Move с одной точкой
  • Line с двумя точками
  • Cubic с четырьмя точками
  • Quad с тремя точками
  • Conic с тремя точками (а также вызовите ConicWeight метод для веса)
  • Close с одной точкой
  • Done

Команда указывает, что перечисление Done пути завершено.

Обратите внимание, что нет Arc глаголов. Это означает, что все дуги преобразуются в кривые Bézier при добавлении в путь.

Некоторые сведения в массиве SKPoint избыточны. Например, если Move за командой Line следует глагол, то первая из двух точек, сопровождающих Line ее, совпадает с Move точкой. На практике эта избыточность очень полезна. При получении Cubic команды он сопровождается всеми четырьмя точками, определяющими кубическую кривую Bézier. Текущее положение, установленное предыдущей командой, не требуется.

Проблематичная команда, однако, является Close. Эта команда рисует прямую линию от текущей позиции к началу контура, установленного ранее командой Move . В идеале Close команда должна предоставлять эти два пункта, а не только одну точку. Хуже того, что точка, сопровождающая Close глагол, всегда (0, 0). При перечислении по пути, вероятно, потребуется сохранить Move точку и текущую позицию.

Перечисление, сплошение и неправильный формат

Иногда желательно применить алгоритмическое преобразование к пути к неправильной форме:

Текст, завернутый в полушарие

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

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

Чтобы помочь в этом процессе, пример содержит статический PathExtensions класс с Interpolate методом, который разбивает прямую линию на многочисленные короткие строки, которые являются только одной единицей длины. Кроме того, класс содержит несколько методов, которые преобразуют три типа кривых Bézier в ряд крошечных прямых линий, которые приблизит кривую. (Формулы параметрики представлены в статье Три типа кривых Bézier.) Этот процесс называется плоской кривой:

static class PathExtensions
{
    ...
    static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * pt0.X + t * pt1.X;
            float y = (1 - t) * pt0.Y + t * pt1.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
                        3 * t * (1 - t) * (1 - t) * pt1.X +
                        3 * t * t * (1 - t) * pt2.X +
                        t * t * t * pt3.X;
            float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
                        3 * t * (1 - t) * (1 - t) * pt1.Y +
                        3 * t * t * (1 - t) * pt2.Y +
                        t * t * t * pt3.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            x /= denominator;
            y /= denominator;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static double Length(SKPoint pt0, SKPoint pt1)
    {
        return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
    }
}

Все эти методы ссылаются на метод CloneWithTransform расширения, также включены в этот класс и показаны ниже. Этот метод клонирует путь, перечисляя команды пути и создавая новый путь на основе данных. Однако новый путь состоит только из вызовов MoveTo и LineTo вызовов. Все кривые и прямые линии сокращаются до ряда крошечных линий.

При вызове CloneWithTransformметод передается методу Func<SKPoint, SKPoint>, который является функцией с SKPaint параметром, возвращающим SKPoint значение. Эта функция вызывается для каждой точки для применения пользовательского алгоритмического преобразования:

static class PathExtensions
{
    public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
    {
        SKPath pathOut = new SKPath();

        using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
        {
            SKPoint[] points = new SKPoint[4];
            SKPathVerb pathVerb = SKPathVerb.Move;
            SKPoint firstPoint = new SKPoint();
            SKPoint lastPoint = new SKPoint();

            while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
            {
                switch (pathVerb)
                {
                    case SKPathVerb.Move:
                        pathOut.MoveTo(transform(points[0]));
                        firstPoint = lastPoint = points[0];
                        break;

                    case SKPathVerb.Line:
                        SKPoint[] linePoints = Interpolate(points[0], points[1]);

                        foreach (SKPoint pt in linePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[1];
                        break;

                    case SKPathVerb.Cubic:
                        SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);

                        foreach (SKPoint pt in cubicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[3];
                        break;

                    case SKPathVerb.Quad:
                        SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);

                        foreach (SKPoint pt in quadPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Conic:
                        SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());

                        foreach (SKPoint pt in conicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Close:
                        SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);

                        foreach (SKPoint pt in closePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        firstPoint = lastPoint = new SKPoint(0, 0);
                        pathOut.Close();
                        break;
                }
            }
        }
        return pathOut;
    }
    ...
}

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

Обратите внимание, что метод сохраняет первую точку каждого контура в переменной, вызываемой firstPoint и текущей позиции после каждой команды рисования в переменной lastPoint. Эти переменные необходимы для создания окончательной закрывающей строки при Close обнаружении команды.

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

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

Конструктор GlobularTextPage классов выполняет это преобразование. Он создает SKPaint объект для текста, а затем получает SKPath объект из GetTextPath метода. Это путь, передаваемый методу CloneWithTransform расширения вместе с функцией преобразования:

public class GlobularTextPage : ContentPage
{
    SKPath globePath;

    public GlobularTextPage()
    {
        Title = "Globular Text";

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

        using (SKPaint textPaint = new SKPaint())
        {
            textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
            textPaint.TextSize = 100;

            using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
            {
                SKRect textPathBounds;
                textPath.GetBounds(out textPathBounds);

                globePath = textPath.CloneWithTransform((SKPoint pt) =>
                {
                    double longitude = (Math.PI / textPathBounds.Width) *
                                            (pt.X - textPathBounds.Left) - Math.PI / 2;
                    double latitude = (Math.PI / textPathBounds.Height) *
                                            (pt.Y - textPathBounds.Top) - Math.PI / 2;

                    longitude *= 0.75;
                    latitude *= 0.75;

                    float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
                    float y = (float)Math.Sin(latitude);

                    return new SKPoint(x, y);
                });
            }
        }
    }
    ...
}

Функция преобразования сначала вычисляет два значения с именем longitude и latitude диапазон от –π/2 в верхней и левой части текста до π/2 в правом и нижнем углу текста. Диапазон этих значений не является визуально удовлетворительно, поэтому они сокращаются путем умножения на 0,75. (Попробуйте код без этих настроек. Текст становится слишком неясным на северном и южном полюсах, и слишком тонким на сторонах.) Эти трехмерные сферические координаты преобразуются в двухмерные x и y координаты по стандартным формулам.

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

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

        canvas.Clear();

        using (SKPaint pathPaint = new SKPaint())
        {
            pathPaint.Style = SKPaintStyle.Fill;
            pathPaint.Color = SKColors.Blue;
            pathPaint.StrokeWidth = 3;
            pathPaint.IsAntialias = true;

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.Scale(0.45f * Math.Min(info.Width, info.Height));     // radius
            canvas.DrawPath(globePath, pathPaint);
        }
    }
}

Это очень универсальный метод. Если массив эффектов пути, описанных в статье "Эффекты пути", не вполне охватывает то, что вы чувствовали, следует включить, это способ заполнить пробелы.