Udostępnij za pośrednictwem


Wyliczanie i informacje o ścieżce

Uzyskiwanie informacji o ścieżkach i wyliczanie zawartości

Klasa SKPath definiuje kilka właściwości i metod, które umożliwiają uzyskanie informacji o ścieżce. Właściwości Bounds i TightBounds (i powiązane metody) uzyskują wymiary metrowe ścieżki. Metoda Contains umożliwia określenie, czy określony punkt znajduje się w ścieżce.

Czasami przydatne jest określenie całkowitej długości wszystkich linii i krzywych tworzących ścieżkę. Obliczanie tej długości nie jest algorytmicznie prostym zadaniem, więc cała klasa o nazwie PathMeasure jest mu poświęcona.

Czasami przydatne jest również uzyskanie wszystkich operacji rysowania i punktów tworzących ścieżkę. Na początku ta funkcja może wydawać się niepotrzebna: Jeśli program utworzył ścieżkę, program już zna zawartość. Wiesz jednak, że ścieżki mogą być również tworzone przez efekty ścieżki i przez konwertowanie ciągów tekstowych na ścieżki. Można również uzyskać wszystkie operacje i punkty rysunku, które tworzą te ścieżki. Jedną z możliwości jest zastosowanie przekształcenia algorytmicznego do wszystkich punktów, na przykład w celu opakowania tekstu wokół półkuli:

Tekst owinięty na półkuli

Pobieranie długości ścieżki

W artykule Ścieżki i tekst pokazano, jak za pomocą DrawTextOnPath metody narysować ciąg tekstowy, którego punkt odniesienia jest zgodny z przebiegiem ścieżki. Ale co zrobić, jeśli chcesz rozmiar tekstu tak, aby dokładnie pasował do ścieżki? Rysowanie tekstu wokół okręgu jest łatwe, ponieważ obwód okręgu jest prosty do obliczenia. Ale obwód wielokropka lub długość krzywej Béziera nie jest tak prosta.

Klasa SKPathMeasure może pomóc. Konstruktor akceptuje SKPath argument, a Length właściwość ujawnia jego długość.

Ta klasa jest pokazana w przykładzie Długość ścieżki , która jest oparta na stronie Krzywa Beziera. Plik PathLengthPage.xaml pochodzi z InteractivePage pliku i zawiera interfejs dotykowy:

<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>

Plik PathLengthPage.xaml.cs kodu umożliwia przenoszenie czterech punktów dotykowych w celu zdefiniowania punktów końcowych i punktów kontrolnych krzywej Béziera sześciennego. Trzy pola definiują ciąg tekstowy, SKPaint obiekt i szerokość obliczeniową tekstu:

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);
    ...
}

Pole baseTextWidth jest szerokością tekstu na TextSize podstawie ustawienia 10.

Program PaintSurface obsługi rysuje krzywą Bézier, a następnie rozmiaruje tekst, aby zmieścił się wzdłuż pełnej długości:

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);
    }
    ...
}

Właściwość Length nowo utworzonego SKPathMeasure obiektu uzyskuje długość ścieżki. Długość ścieżki jest podzielona przez baseTextWidth wartość (czyli szerokość tekstu na podstawie rozmiaru tekstu 10), a następnie pomnożona przez rozmiar tekstu podstawowego 10. Wynik jest nowym rozmiarem tekstu do wyświetlania tekstu wzdłuż tej ścieżki:

Potrójny zrzut ekranu przedstawiający stronę Długość ścieżki

Ponieważ krzywa Béziera jest dłuższa lub krótsza, można zobaczyć zmianę rozmiaru tekstu.

Przechodzenie ścieżki

SKPathMeasure może zrobić więcej niż tylko zmierzyć długość ścieżki. W przypadku dowolnej wartości z zakresu od zera do długości SKPathMeasure ścieżki obiekt może uzyskać pozycję na ścieżce i tangens do krzywej ścieżki w tym momencie. Tangens jest dostępny jako wektor w postaci SKPoint obiektu lub jako obrót hermetyzowany w SKMatrix obiekcie. Poniżej przedstawiono metody uzyskiwania SKPathMeasure tych informacji w różny i elastyczny sposób:

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 Członkowie wyliczenia to:

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

Strona Unicycle Half-Pipe animuje figurę kija na unicycle, który wydaje się jeździć tam iz powrotem wzdłuż sześciennej krzywej Béziera:

Potrójny zrzut ekranu przedstawiający stronę Pół potoku unicycle

Obiekt SKPaint używany do strojania zarówno pół potoku, jak i mononicycle jest definiowany jako pole w UnicycleHalfPipePage klasie. Zdefiniowany również obiekt jest obiektem SKPath dla jednokleru:

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");
    ...
}

Klasa zawiera standardowe przesłonięcia OnAppearing metod i OnDisappearing dla animacji. Procedura PaintSurface obsługi tworzy ścieżkę dla potoku pół potoku, a następnie rysuje ją. Następnie SKPathMeasure obiekt jest tworzony na podstawie tej ścieżki:

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

Procedura PaintSurface obsługi oblicza wartość z przedziału t od 0 do 1 co pięć sekund. Następnie używa Math.Cos funkcji , aby przekonwertować ją na wartość t z zakresu od 0 do 1 i z powrotem do 0, gdzie 0 odpowiada unicycle na początku po lewej stronie górnej, podczas gdy 1 odpowiada unicycle w prawym górnym rogu. Cosinus funkcja powoduje spowolnienie prędkości u góry rury i najszybsze na dole.

Zwróć uwagę, że ta wartość t musi być pomnożona przez długość ścieżki dla pierwszego argumentu na GetMatrixwartość . Macierz jest następnie stosowana do SKCanvas obiektu w celu rysowania ścieżki unicycle.

Wyliczanie ścieżki

Dwie osadzone klasy SKPath umożliwiają wyliczanie zawartości ścieżki. Te klasy to SKPath.Iterator i SKPath.RawIterator. Dwie klasy są bardzo podobne, ale SKPath.Iterator mogą wyeliminować elementy w ścieżce o zerowej długości lub blisko zerowej długości. Element RawIterator jest używany w poniższym przykładzie.

Obiekt typu SKPath.RawIterator można uzyskać, wywołując metodę CreateRawIteratorSKPath. Wyliczanie ścieżki odbywa się przez wielokrotne wywoływanie Next metody . Przekaż do niej tablicę czterech SKPoint wartości:

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

Metoda Next zwraca element członkowski SKPathVerb typu wyliczenia. Te wartości wskazują konkretne polecenie rysunku w ścieżce. Liczba prawidłowych punktów wstawionych w tablicy zależy od tego zlecenia:

  • Move z pojedynczym punktem
  • Line z dwoma punktami
  • Cubic z czterema punktami
  • Quad z trzema punktami
  • Conic z trzema punktami (a także wywołaj metodę ConicWeight wagi)
  • Close z jednym punktem
  • Done

Czasownik Done wskazuje, że wyliczenie ścieżki zostało ukończone.

Zwróć uwagę, że nie Arc ma żadnych czasowników. Oznacza to, że wszystkie łuki są konwertowane na krzywe Bézier po dodaniu do ścieżki.

Niektóre informacje w tablicy SKPoint są nadmiarowe. Jeśli na przykład Move czasownik jest po Line nim czasownik, pierwszy z dwóch punktów towarzyszących Line jest taki sam jak Move punkt. W praktyce ta nadmiarowość jest bardzo pomocna. Gdy otrzymujesz Cubic czasownik, towarzyszy mu wszystkie cztery punkty definiujące krzywą Sześciennego Béziera. Nie musisz zachować bieżącego położenia ustalonego przez poprzednie zlecenie.

Problematyczne czasowniki to Closejednak . To polecenie rysuje linię prostą z bieżącej pozycji na początek konturu ustalonego Move wcześniej przez polecenie . W idealnym przypadku czasownik powinien podać te dwa punkty, Close a nie tylko jeden punkt. Co gorsza, punkt towarzyszący Close czasownikowi jest zawsze (0, 0). Podczas wyliczania ścieżki prawdopodobnie trzeba zachować Move punkt i bieżącą pozycję.

Wyliczanie, spłaszczanie i źle sformułowanie

Czasami pożądane jest zastosowanie transformacji algorytmicznej do ścieżki, aby ją źle sformułować w jakiś sposób:

Tekst owinięty na półkuli

Większość tych liter składa się z prostych linii, ale te linie proste najwyraźniej zostały skręcone w krzywe. Jak to możliwe?

Kluczem jest to, że oryginalne linie proste są podzielone na serię mniejszych linii prostych. Te mniejsze linie proste można następnie manipulować na różne sposoby, aby utworzyć krzywą.

Aby ułatwić ten proces, przykład zawiera klasę statyczną PathExtensions z Interpolate metodą, która dzieli linię prostą na wiele krótkich wierszy, które są tylko jedną jednostką długości. Ponadto klasa zawiera kilka metod, które konwertują trzy typy krzywych Bézier na serię małych linii prostych, które przybliżają krzywą. (Formuły parametryczne zostały przedstawione w artykule Trzy typy krzywych Béziera. Ten proces jest nazywany spłaszczanie krzywej:

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

Wszystkie te metody są przywołyne z metody CloneWithTransform rozszerzenia również zawarte w tej klasie i pokazane poniżej. Ta metoda klonuje ścieżkę, wyliczając polecenia ścieżki i tworząc nową ścieżkę na podstawie danych. Jednak nowa ścieżka składa się tylko z MoveTo wywołań i LineTo . Wszystkie krzywe i linie proste są zredukowane do serii małych linii.

Podczas wywoływania CloneWithTransformmetody należy przekazać metodę Func<SKPoint, SKPoint>, która jest funkcją z parametrem SKPaint , który zwraca SKPoint wartość. Ta funkcja jest wywoływana dla każdego punktu w celu zastosowania niestandardowej transformacji algorytmicznej:

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;
    }
    ...
}

Ponieważ sklonowana ścieżka jest ograniczona do małych linii prostych, funkcja przekształcania ma możliwość konwertowania linii prostych na krzywe.

Zwróć uwagę, że metoda zachowuje pierwszy punkt każdego konturu w zmiennej o nazwie firstPoint i bieżące położenie po każdym poleceniu rysunku w zmiennej lastPoint. Te zmienne są niezbędne do konstruowania końcowego wiersza zamknięcia, gdy Close napotkano czasownik.

Przykład GlobularText używa tej metody rozszerzenia, aby pozornie zawijać tekst wokół półkuli w efekt 3D:

Potrójny zrzut ekranu przedstawiający stronę Tekst globularny

Konstruktor GlobularTextPage klasy wykonuje tę transformację. SKPaint Tworzy obiekt dla tekstu, a następnie uzyskuje SKPath obiekt z GetTextPath metody . Jest to ścieżka przekazana do CloneWithTransform metody rozszerzenia wraz z funkcją transform:

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);
                });
            }
        }
    }
    ...
}

Funkcja transform najpierw oblicza dwie wartości o nazwie longitude i latitude od –π/2 w górnej i lewej części tekstu, aby π/2 po prawej i dolnej części tekstu. Zakres tych wartości nie jest wizualnie zadowalający, dlatego są one zmniejszane przez pomnożenie przez 0,75. (Wypróbuj kod bez tych korekt. Tekst staje się zbyt niejasny na biegunach północnych i południowych i zbyt cienkich po bokach). Te trójwymiarowe współrzędne sfericzne są konwertowane na dwuwymiarowe x i y współrzędne według standardowych formuł.

Nowa ścieżka jest przechowywana jako pole. Następnie PaintSurface program obsługi musi jedynie wyśrodkować i skalować ścieżkę, aby wyświetlić ją na ekranie:

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

Jest to bardzo wszechstronna technika. Jeśli tablica efektów ścieżki opisanych w artykule Path Effects nie obejmuje czegoś, co należy uwzględnić, jest to sposób na wypełnienie luk.