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:
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 LineTo
i 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 null
wartość . 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:
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:
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: