Udostępnij za pośrednictwem


Dane ścieżki SVG w usłudze SkiaSharp

Definiowanie ścieżek przy użyciu ciągów tekstowych w formacie skalowalnej grafiki wektorowej

Klasa SKPath obsługuje definicję całych obiektów ścieżki z ciągów tekstowych w formacie określonym przez specyfikację Skalowalnej grafiki wektorowej (SVG). W dalszej części tego artykułu zobaczysz, jak można reprezentować całą ścieżkę, taką jak ta w ciągu tekstowym:

Przykładowa ścieżka zdefiniowana przy użyciu danych ścieżki SVG

SVG to język programowania graficznego opartego na języku XML dla stron internetowych. Ze względu na to, że svG musi zezwalać na definiowanie ścieżek w znaczników, a nie w serii wywołań funkcji, standard SVG zawiera niezwykle zwięzły sposób określania całej ścieżki grafiki jako ciągu tekstowego.

W programie SkiaSharp ten format jest określany jako "dane ścieżki SVG". Format jest również obsługiwany w środowiskach programowania opartych na języku XAML systemu Windows, w tym Windows Presentation Foundation i platforma uniwersalna systemu Windows, gdzie jest znany jako składnia znaczników ścieżki lub składnia przenoszenia i rysowania poleceń. Może również służyć jako format wymiany obrazów grafiki wektorowej, szczególnie w plikach tekstowych, takich jak XML.

Klasa SKPath definiuje dwie metody z wyrazami SvgPathData w nazwach:

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

Metoda statyczna ParseSvgPathData konwertuje ciąg na SKPath obiekt, podczas gdy ToSvgPathData konwertuje SKPath obiekt na ciąg.

Oto ciąg SVG dla pięcioramiennej gwiazdy wyśrodkowanej na punkcie (0, 0) z promieniem 100:

"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"

Litery to polecenia, które tworzą SKPath obiekt: M wskazuje MoveTo wywołanie , L jest LineToi Z ma Close zamknąć kontur. Każda para liczb zapewnia współrzędną X i Y punktu. Zwróć uwagę, że L polecenie następuje po wielu punktach rozdzielonych przecinkami. W serii współrzędnych i punktów przecinki i odstępy są traktowane identycznie. Niektórzy programiści wolą umieszczać przecinki między współrzędnymi X i Y, a nie między punktami, ale przecinki lub spacje są wymagane tylko w celu uniknięcia niejednoznaczności. Jest to całkowicie legalne:

"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"

Składnia danych ścieżki SVG jest formalnie udokumentowana w sekcji 8.3 specyfikacji SVG. Oto podsumowanie:

MoveTo

M x y

Spowoduje to rozpoczęcie nowego konturu w ścieżce przez ustawienie bieżącej pozycji. Dane ścieżki powinny zawsze zaczynać się od M polecenia .

LineTo

L x y ...

To polecenie dodaje wiersz prosty (lub wiersze) do ścieżki i ustawia nowe bieżące położenie na końcu ostatniego wiersza. Możesz stosować L polecenie z wieloma parami współrzędnych x i y .

Linia pozioma Do

H x ...

To polecenie dodaje linię poziomą do ścieżki i ustawia nową bieżącą pozycję na końcu wiersza. Możesz wykonać polecenie H z wieloma współrzędnymi x , ale nie ma sensu.

Linia pionowa

V y ...

To polecenie dodaje pionowy wiersz do ścieżki i ustawia nowe bieżące położenie na końcu wiersza.

Zamknij

Z

Polecenie C zamyka kontur, dodając linię prostą z bieżącej pozycji do początku konturu.

ArcTo

Polecenie dodawania łuku wielokropkowego do konturu jest zdecydowanie najbardziej złożonym poleceniem w całej specyfikacji danych ścieżki SVG. Jest to jedyne polecenie, w którym liczby mogą reprezentować coś innego niż wartości współrzędnych:

A rx ry rotation-angle large-arc-flag sweep-flag x y ...

Parametry rx i ry to poziome i pionowe promienie wielokropka. Kąt obrotu jest zgodnie z ruchem wskazówek zegara w stopniach.

Ustaw flagę duży łuk na 1 dla dużego łuku lub wartość 0 dla małego łuku.

Ustaw flagę zamiatania na 1 dla ruchu wskazówek zegara i wartość 0 dla licznika zgodnie z ruchem wskazówek zegara.

Łuk jest przyciągany do punktu (x, y), który staje się nową bieżącą pozycją.

Sześcienna Do

C x1 y1 x2 y2 x3 y3 ...

To polecenie dodaje krzywą sześcienną Béziera z bieżącej pozycji do (x3, y3), która staje się nową bieżącą pozycją. Punkty (x1, y1) i (x2, y2) to punkty kontrolne.

Wiele krzywych Bézier można określić za pomocą jednego C polecenia. Liczba punktów musi być wielokrotna 3.

Istnieje również polecenie "smooth" Bézier curve:

S x2 y2 x3 y3 ...

To polecenie powinno być zgodne ze zwykłym poleceniem Bézier (chociaż nie jest to ściśle wymagane). Smooth Bézier polecenie oblicza pierwszy punkt kontrolny, tak aby był odbiciem drugiego punktu kontrolnego poprzedniego Béziera wokół ich wzajemnego punktu. Te trzy punkty są zatem grube, a połączenie między dwoma krzywymi Béziera jest gładkie.

QuadTo

Q x1 y1 x2 y2 ...

W przypadku krzywych kwadratowej Béziera liczba punktów musi być wielokrotną liczbą 2. Punkt kontrolny to (x1, y1) i punkt końcowy (i nowa bieżąca pozycja) to (x2, y2)

Istnieje również gładkie polecenie krzywej kwadratowej:

T x2 y2 ...

Punkt kontrolny jest obliczany na podstawie punktu kontrolnego poprzedniej krzywej kwadratowej.

Wszystkie te polecenia są również dostępne w wersjach względnych, w których punkty współrzędnych są względne względem bieżącego położenia. Te względne polecenia zaczynają się od małych liter, na przykład c zamiast C względnej wersji sześciennego polecenia Bézier.

Jest to zakres definicji danych ścieżki SVG. Nie ma możliwości powtarzania grup poleceń ani wykonywania dowolnego typu obliczeń. Polecenia dla ConicTo lub innych typów specyfikacji łuku nie są dostępne.

Metoda statyczna SKPath.ParseSvgPathData oczekuje prawidłowego ciągu poleceń SVG. Jeśli zostanie wykryty jakikolwiek błąd składniowy, metoda zwraca nullwartość . Jest to jedyne wskazanie błędu.

Metoda ToSvgPathData jest przydatna do uzyskiwania danych ścieżki SVG z istniejącego SKPath obiektu do transferu do innego programu lub przechowywania w formacie pliku tekstowym, takim jak XML. (Metoda ToSvgPathData nie jest pokazana w przykładowym kodzie w tym artykule). Nie należy oczekiwać ToSvgPathData zwrócenia ciągu odpowiadającego dokładnie wywołaniom metody, które utworzyły ścieżkę. W szczególności dowiesz się, że łuki są konwertowane na wiele QuadTo poleceń i tak wyglądają w danych ścieżki zwróconych z ToSvgPathData.

Strona Witaj dane ścieżki określa słowo "HELLO" przy użyciu danych ścieżki SVG. SKPath Obiekty i SKPaint są definiowane jako pola w PathDataHelloPage klasie :

public class PathDataHelloPage : ContentPage
{
    SKPath helloPath = SKPath.ParseSvgPathData(
        "M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100" +                // H
        "M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100" +     // E
        "M 150 0 L 150 100, 200 100" +                                  // L
        "M 225 0 L 225 100, 275 100" +                                  // L
        "M 300 50 A 25 50 0 1 0 300 49.9 Z");                           // O

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = SKStrokeCap.Round,
        StrokeJoin = SKStrokeJoin.Round
    };
    ...
}

Ścieżka definiująca ciąg tekstowy rozpoczyna się w lewym górnym rogu w punkcie (0, 0). Każda litera ma 50 jednostek szerokości i 100 jednostek wysokości, a litery są oddzielone kolejnymi 25 jednostkami, co oznacza, że cała ścieżka ma 350 jednostek szerokości.

"H" "Hello" składa się z trzech konturów jednowierszowych, podczas gdy "E" jest dwie połączone sześcienne krzywe Bézier. Zwróć uwagę, że C polecenie następuje sześć punktów, a dwa punkty kontrolne mają współrzędne Y –10 i 110, co stawia je poza zakresem współrzędnych Y innych liter. "L" to dwa połączone wiersze, a "O" to wielokropek renderowany za pomocą A polecenia .

Zwróć uwagę, że M polecenie rozpoczynające ostatni kontur ustawia położenie na punkt (350, 50), czyli pionowy środek lewej strony "O". Zgodnie z pierwszymi liczbami po A poleceniu, wielokropek ma poziomy promień 25 i pionowy promień 50. Punkt końcowy jest wskazywany przez ostatnią parę liczb w poleceniu A , który reprezentuje punkt (300, 49,9). To celowo po prostu nieco różni się od punktu początkowego. Jeśli punkt końcowy jest ustawiony na punkt początkowy, łuk nie zostanie renderowany. Aby narysować pełny wielokropek, należy ustawić punkt końcowy w pobliżu (ale nie równy) punktu początkowego lub należy użyć co najmniej dwóch A poleceń, z których każda jest częścią kompletnego wielokropka.

Możesz dodać następującą instrukcję do konstruktora strony, a następnie ustawić punkt przerwania, aby zbadać wynikowy ciąg:

string str = helloPath.ToSvgPathData();

Dowiesz się, że łuk został zastąpiony długą serią Q poleceń dla fragmentalnego przybliżenia łuku przy użyciu krzywych kwadratowych Béziera.

Procedura PaintSurface obsługi uzyskuje ścisłe granice ścieżki, która nie obejmuje punktów kontrolnych dla krzywych "E" i "O". Trzy przekształcenia przenoszą środek ścieżki do punktu (0, 0), skaluj ścieżkę do rozmiaru kanwy (ale także biorąc pod uwagę szerokość pociągnięcia), a następnie przenieś środek ścieżki do środka kanwy:

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

        canvas.Clear();

        SKRect bounds;
        helloPath.GetTightBounds(out bounds);

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

        canvas.Scale(info.Width / (bounds.Width + paint.StrokeWidth),
                     info.Height / (bounds.Height + paint.StrokeWidth));

        canvas.Translate(-bounds.MidX, -bounds.MidY);

        canvas.DrawPath(helloPath, paint);
    }
}

Ścieżka wypełnia kanwę, która wygląda bardziej rozsądnie po wyświetleniu w trybie poziomym:

Potrójny zrzut ekranu przedstawiający stronę Witaj dane ścieżki

Strona Kategoria danych ścieżki jest podobna. Ścieżki i obiekty malowania są definiowane jako pola w PathDataCatPage klasie:

public class PathDataCatPage : ContentPage
{
    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 paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Orange,
        StrokeWidth = 5
    };
    ...
}

Głowa kota jest okręgiem, a tutaj jest renderowana z dwoma A poleceniami, z których każdy rysuje półokrąg. Oba A polecenia dla głowy definiują poziome i pionowe promienie 100. Pierwszy łuk zaczyna się od (240, 100) i kończy się na (240, 300), co staje się punktem początkowym drugiego łuku, który kończy się z powrotem (240, 100).

Dwa oczy są również renderowane za pomocą dwóch A poleceń, a podobnie jak w przypadku głowy kota, drugie A polecenie kończy się w tym samym punkcie co początek pierwszego A polecenia. Jednak te pary A poleceń nie definiują wielokropka. Z każdym łukiem jest 40 jednostek, a promień wynosi również 40 jednostek, co oznacza, że łuki te nie są pełne półokrągów.

Procedura PaintSurface obsługi wykonuje podobne przekształcenia jak w poprzednim przykładzie, ale ustawia jeden Scale czynnik, aby zachować współczynnik proporcji i zapewnić mały margines, aby wąsy kota nie dotykały boków ekranu:

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

        canvas.Clear(SKColors.Black);

        SKRect bounds;
        catPath.GetBounds(out bounds);

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

        canvas.Scale(0.9f * Math.Min(info.Width / bounds.Width,
                                     info.Height / bounds.Height));

        canvas.Translate(-bounds.MidX, -bounds.MidY);

        canvas.DrawPath(catPath, paint);
    }
}

Oto uruchomiony program:

Potrójny zrzut ekranu przedstawiający stronę Cat danych ścieżki

Zwykle, gdy SKPath obiekt jest zdefiniowany jako pole, kontury ścieżki muszą być zdefiniowane w konstruktorze lub innej metodzie. Jednak w przypadku korzystania z danych ścieżki SVG widać, że ścieżkę można określić całkowicie w definicji pola.

Wcześniejsza próbka Ugly Analog Clock w artykule Obracanie transformacji wyświetlał ręce zegara jako proste linie. Poniższy program Pretty Analog Clock zastępuje te wiersze obiektami zdefiniowanymi SKPath jako pola w PrettyAnalogClockPage klasie wraz z obiektami SKPaint :

public class PrettyAnalogClockPage : ContentPage
{
    ...
    // Clock hands pointing straight up
    SKPath hourHandPath = SKPath.ParseSvgPathData(
        "M 0 -60 C   0 -30 20 -30  5 -20 L  5   0" +
                "C   5 7.5 -5 7.5 -5   0 L -5 -20" +
                "C -20 -30  0 -30  0 -60 Z");

    SKPath minuteHandPath = SKPath.ParseSvgPathData(
        "M 0 -80 C   0 -75  0 -70  2.5 -60 L  2.5   0" +
                "C   2.5 5 -2.5 5 -2.5   0 L -2.5 -60" +
                "C 0 -70  0 -75  0 -80 Z");

    SKPath secondHandPath = SKPath.ParseSvgPathData(
        "M 0 10 L 0 -80");

    // SKPaint objects
    SKPaint handStrokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 2,
        StrokeCap = SKStrokeCap.Round
    };

    SKPaint handFillPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Gray
    };
    ...
}

Godziny i minuty ręce mają teraz zamknięte obszary. Aby te ręce były odrębne od siebie, są one rysowane zarówno z czarnym konturem, jak i szarym wypełnieniem przy użyciu handStrokePaint obiektów i handFillPaint .

We wcześniejszej próbce Ugly Analog Clock małe okręgi, które oznaczyły godziny i minuty, zostały narysowane w pętli. W tym przykładzie Pretty Analog Clock jest używane zupełnie inne podejście: znaczniki godziny i minuty są kropkowane linie rysowane z minuteMarkPaint obiektami i hourMarkPaint :

public class PrettyAnalogClockPage : ContentPage
{
    ...
    SKPaint minuteMarkPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 3,
        StrokeCap = SKStrokeCap.Round,
        PathEffect = SKPathEffect.CreateDash(new float[] { 0, 3 * 3.14159f }, 0)
    };

    SKPaint hourMarkPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Black,
        StrokeWidth = 6,
        StrokeCap = SKStrokeCap.Round,
        PathEffect = SKPathEffect.CreateDash(new float[] { 0, 15 * 3.14159f }, 0)
    };
    ...
}

W artykule Kropki i kreski omówiono sposób tworzenia linii przerywanej za pomocą SKPathEffect.CreateDash metody . Pierwszym argumentem jest tablica, float która zazwyczaj zawiera dwa elementy: Pierwszy element to długość kreski, a drugi element jest szczeliną między kreskami. StrokeCap Gdy właściwość jest ustawiona na SKStrokeCap.Round, zaokrąglone końce kreski skutecznie wydłużają długość kreski o szerokość pociągnięcia po obu stronach kreski. W związku z tym ustawienie pierwszego elementu tablicy na wartość 0 powoduje utworzenie linii kropkowanej.

Odległość między tymi kropkami jest określana przez drugi element tablicy. Jak zobaczysz wkrótce, te dwa SKPaint obiekty są używane do rysowania okręgów z promieniem 90 jednostek. Obwód tego okręgu jest zatem 180π, co oznacza, że znaczniki 60 minut muszą być wyświetlane co 3 jednostkiπ, co jest drugą wartością w tablicy float w minuteMarkPaint. Znaczniki 12 godzin muszą być wyświetlane co 15 jednostekπ, co jest wartością w drugiej float tablicy.

Klasa PrettyAnalogClockPage ustawia czasomierz, aby unieważnić powierzchnię co 16 milisekund, a PaintSurface procedura obsługi jest wywoływana w tym tempie. Wcześniejsze definicje SKPath obiektów i SKPaint umożliwiają bardzo czysty kod rysunku:

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

        canvas.Clear();

        // Transform for 100-radius circle in center
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(Math.Min(info.Width / 200, info.Height / 200));

        // Draw circles for hour and minute marks
        SKRect rect = new SKRect(-90, -90, 90, 90);
        canvas.DrawOval(rect, minuteMarkPaint);
        canvas.DrawOval(rect, hourMarkPaint);

        // Get time
        DateTime dateTime = DateTime.Now;

        // Draw hour hand
        canvas.Save();
        canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
        canvas.DrawPath(hourHandPath, handStrokePaint);
        canvas.DrawPath(hourHandPath, handFillPaint);
        canvas.Restore();

        // Draw minute hand
        canvas.Save();
        canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
        canvas.DrawPath(minuteHandPath, handStrokePaint);
        canvas.DrawPath(minuteHandPath, handFillPaint);
        canvas.Restore();

        // Draw second hand
        double t = dateTime.Millisecond / 1000.0;

        if (t < 0.5)
        {
            t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
        }
        else
        {
            t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
        }

        canvas.Save();
        canvas.RotateDegrees(6 * (dateTime.Second + (float)t));
        canvas.DrawPath(secondHandPath, handStrokePaint);
        canvas.Restore();
    }
}

Coś specjalnego jest jednak zrobione z drugiej ręki. Ponieważ zegar jest aktualizowany co 16 milisekund, Millisecond właściwość DateTime wartości może być potencjalnie używana do animowania zamiatania drugiej ręki zamiast jednej, która porusza się w dyskretnych skokach od sekundy do sekundy. Jednak ten kod nie pozwala na płynne przenoszenie. Zamiast tego używa Xamarin.FormsSpringIn funkcji złagodzenia animacji i SpringOut dla innego rodzaju ruchu. Te funkcje złagodzenia powodują, że druga ręka porusza się w sposób drżący — cofając się trochę przed przeniesieniem, a następnie nieco nadmiernie strzelając do miejsca docelowego, efekt, który niestety nie można odtworzyć na tych statycznych zrzutach ekranu:

Potrójny zrzut ekranu przedstawiający stronę Pretty Analog Clock