SVG-Pfaddaten in SkiaSharp
Definieren von Pfaden mithilfe von Textzeichenfolgen im Format Skalierbare Vektorgrafiken
Die SKPath
Klasse unterstützt die Definition von gesamten Pfadobjekten aus Textzeichenfolgen in einem Format, das durch die SVG-Spezifikation (Scalable Vector Graphics) festgelegt wurde. Weiter unten in diesem Artikel erfahren Sie, wie Sie einen ganzen Pfad wie diesen in einer Textzeichenfolge darstellen können:
SVG ist eine XML-basierte Grafikprogrammiersprache für Webseiten. Da SVG die Definition von Pfaden im Markup statt einer Reihe von Funktionsaufrufen zulassen muss, enthält der SVG-Standard eine äußerst präzise Methode zum Angeben eines gesamten Grafikpfads als Textzeichenfolge.
Innerhalb von SkiaSharp wird dieses Format als "SVG-Pfaddaten" bezeichnet. Das Format wird auch in Windows-XAML-basierten Programmierumgebungen unterstützt, einschließlich der Windows Presentation Foundation und der Universelle Windows-Plattform, wobei es als Pfadmarkierungssyntax oder die Syntax der Befehle verschieben und zeichnen bezeichnet wird. Sie kann auch als Austauschformat für Vektorgrafikbilder dienen, insbesondere in textbasierten Dateien wie XML.
Die SKPath
Klasse definiert zwei Methoden mit den Wörtern SvgPathData
in ihren Namen:
public static SKPath ParseSvgPathData(string svgPath)
public string ToSvgPathData()
Die statische ParseSvgPathData
Methode konvertiert eine Zeichenfolge in ein SKPath
Objekt, während ToSvgPathData
ein SKPath
Objekt in eine Zeichenfolge konvertiert wird.
Hier ist eine SVG-Zeichenfolge für einen fünfzackigen Stern zentriert auf dem Punkt (0, 0) mit einem Radius von 100:
"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"
Bei den Buchstaben handelt es sich um Befehle, die ein SKPath
Objekt erstellen: M
gibt einen MoveTo
Aufruf an und Z
besteht darinClose
, L
LineTo
ein Kontur zu schließen. Jedes Zahlenpaar stellt eine X- und Y-Koordinate eines Punkts bereit. Beachten Sie, dass auf den L
Befehl mehrere Punkte gefolgt werden, die durch Kommas getrennt sind. In einer Reihe von Koordinaten und Punkten werden Kommas und Leerzeichen identisch behandelt. Einige Programmierer bevorzugen es, Kommas zwischen den X- und Y-Koordinaten anstelle zwischen den Punkten zu platzieren, aber Kommas oder Leerzeichen sind nur erforderlich, um Mehrdeutigkeit zu vermeiden. Dies ist vollkommen legal:
"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"
Die Syntax der SVG-Pfaddaten ist formell in Abschnitt 8.3 der SVG-Spezifikation dokumentiert. Hier ist eine Zusammenfassung:
MoveTo
M x y
Dadurch beginnt eine neue Kontur im Pfad, indem die aktuelle Position festgelegt wird. Pfaddaten sollten immer mit einem M
Befehl beginnen.
LineTo
L x y ...
Mit diesem Befehl wird dem Pfad eine gerade Linie (oder Zeilen) hinzugefügt und die neue aktuelle Position auf das Ende der letzten Zeile festgelegt. Sie können dem L
Befehl mehrere X- und Y-Koordinatenpaare folgen.
Horizontale LinieZu
H x ...
Mit diesem Befehl wird dem Pfad eine horizontale Linie hinzugefügt und die neue aktuelle Position auf das Ende der Zeile festgelegt. Sie können dem H
Befehl mehrere X-Koordinaten folgen, aber es macht nicht viel Sinn.
Vertikale Linie
V y ...
Mit diesem Befehl wird dem Pfad eine vertikale Linie hinzugefügt und die neue aktuelle Position auf das Ende der Zeile festgelegt.
Abschließen
Z
Der C
Befehl schließt die Kontur, indem eine gerade Linie von der aktuellen Position zum Anfang der Kontur hinzugefügt wird.
ArcTo
Der Befehl zum Hinzufügen eines elliptischen Bogens zur Kontur ist bei weitem der komplexeste Befehl in der gesamten SVG-Pfaddatenspezifikation. Es ist der einzige Befehl, in dem Zahlen etwas anderes als Koordinatenwerte darstellen können:
A rx ry rotation-angle large-arc-flag sweep-flag x y ...
Die Parameter rx und ry sind die horizontalen und vertikalen Radien der Ellipse. Der Drehwinkel ist im Uhrzeigersinn in Grad.
Legen Sie die Große Bogenflagge auf 1 für den großen Bogen oder auf 0 für den kleinen Bogen fest.
Legen Sie das Aufräumen auf 1 für den Uhrzeigersinn und auf 0 für den Uhrzeigersinn fest.
Der Bogen wird auf den Punkt (x, y) gezeichnet, der zur neuen aktuellen Position wird.
CubicTo
C x1 y1 x2 y2 x3 y3 ...
Mit diesem Befehl wird eine kubische Bézierkurve von der aktuellen Position zu (x3, y3) hinzugefügt, die zur neuen aktuellen Position wird. Die Punkte (x1, y1) und (x2, y2) sind Kontrollpunkte.
Mehrere Bézierkurven können durch einen einzigen C
Befehl angegeben werden. Die Anzahl der Punkte muss ein Vielfaches von 3 sein.
Es gibt auch einen "glatten" Bézierkurvenbefehl:
S x2 y2 x3 y3 ...
Dieser Befehl sollte einem regulären Bézier-Befehl folgen (obwohl dies nicht unbedingt erforderlich ist). Der glatte Bézier-Befehl berechnet den ersten Kontrollpunkt so, dass es eine Spiegelung des zweiten Kontrollpunkts des vorherigen Bézier um ihren gegenseitigen Punkt darstellt. Diese drei Punkte sind daher kolinear, und die Verbindung zwischen den beiden Bézierkurven ist glatt.
QuadTo
Q x1 y1 x2 y2 ...
Bei quadratischen Bézierkurven muss die Anzahl der Punkte ein Vielfaches von 2 sein. Der Kontrollpunkt ist (x1, y1) und der Endpunkt (und neue aktuelle Position) ist (x2, y2)
Es gibt auch einen Befehl für eine glatte quadratische Kurve:
T x2 y2 ...
Der Kontrollpunkt wird basierend auf dem Kontrollpunkt der vorherigen quadratischen Kurve berechnet.
Alle diese Befehle sind auch in "relativen" Versionen verfügbar, wobei die Koordinatenpunkte relativ zur aktuellen Position sind. Diese relativen Befehle beginnen mit Kleinbuchstaben, z c
. B. anstelle C
der relativen Version des kubischen Bézier-Befehls.
Dies ist der Umfang der SVG-Pfaddatendefinition. Es gibt keine Möglichkeit zum Wiederholen von Befehlsgruppen oder zum Ausführen einer beliebigen Berechnungsart. Befehle für ConicTo
oder andere Arten von Bogenspezifikationen sind nicht verfügbar.
Die statische SKPath.ParseSvgPathData
Methode erwartet eine gültige Zeichenfolge von SVG-Befehlen. Wenn ein Syntaxfehler erkannt wird, gibt die Methode zurück null
. Das ist der einzige Fehlerindikator.
Die ToSvgPathData
Methode ist praktisch, um SVG-Pfaddaten aus einem vorhandenen SKPath
Objekt zu erhalten, um in ein anderes Programm zu übertragen oder in einem textbasierten Dateiformat wie XML zu speichern. (Die ToSvgPathData
Methode wird im Beispielcode in diesem Artikel nicht veranschaulicht.) Erwarten Sie ToSvgPathData
nicht, dass eine Zeichenfolge zurückgegeben wird, die genau den Methodenaufrufen entspricht, die den Pfad erstellt haben. Insbesondere werden Sie feststellen, dass Bögen in mehrere QuadTo
Befehle konvertiert werden und wie sie in den von zurückgegebenen ToSvgPathData
Pfaddaten angezeigt werden.
Auf der Seite "Pfaddaten hello " wird das Wort "HELLO" mithilfe von SVG-Pfaddaten geschrieben. Sowohl die Objekte SKPaint
als auch die SKPath
Objekte werden als Felder in der PathDataHelloPage
Klasse definiert:
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
};
...
}
Der Pfad, der die Textzeichenfolge definiert, beginnt an der oberen linken Ecke am Punkt(0, 0). Jeder Buchstabe ist 50 Einheiten breit und 100 Einheiten hoch, und Buchstaben werden durch weitere 25 Einheiten getrennt, was bedeutet, dass der gesamte Pfad 350 Einheiten breit ist.
Das "H" von "Hello" besteht aus drei einzeiligen Konturen, während das 'E' zwei verbundene kubische Bézierkurven ist. Beachten Sie, dass auf den C
Befehl sechs Punkte folgt, und zwei der Kontrollpunkte haben Y-Koordinaten von –10 und 110, wodurch sie außerhalb des Bereichs der Y-Koordinaten der anderen Buchstaben platziert werden. "L" ist zwei verbundene Linien, während "O" eine Ellipse ist, die mit einem A
Befehl gerendert wird.
Beachten Sie, dass der M
Befehl, der die letzte Kontur beginnt, die Position auf den Punkt (350, 50) festlegt, was die vertikale Mitte der linken Seite des "O" ist. Wie durch die ersten Zahlen nach dem A
Befehl angegeben, weist die Ellipse einen horizontalen Radius von 25 und einen vertikalen Radius von 50 auf. Der Endpunkt wird durch das letzte Zahlenpaar im A
Befehl angegeben, das den Punkt darstellt (300, 49,9). Das unterscheidet sich bewusst nur geringfügig vom Anfangspunkt. Wenn der Endpunkt auf den Startpunkt festgelegt ist, wird der Bogen nicht gerendert. Um eine vollständige Auslassungspunkte zu zeichnen, müssen Sie den Endpunkt nahe (aber nicht gleich) dem Startpunkt festlegen, oder Sie müssen zwei oder mehr A
Befehle verwenden, die jeweils für einen Teil der vollständigen Auslassungspunkte verwendet werden.
Möglicherweise möchten Sie die folgende Anweisung zum Konstruktor der Seite hinzufügen und dann einen Haltepunkt festlegen, um die resultierende Zeichenfolge zu untersuchen:
string str = helloPath.ToSvgPathData();
Sie werden feststellen, dass der Bogen durch eine lange Reihe von Q
Befehlen für eine stückliche Annäherung des Bogens durch quadratische Bézierkurven ersetzt wurde.
Der PaintSurface
Handler ruft die engen Grenzen des Pfads ab, die keine Kontrollpunkte für die Kurven "E" und "O" enthalten. Die drei Transformationen verschieben die Mitte des Pfads zum Punkt (0, 0), skalieren den Pfad auf die Größe des Zeichenbereichs (aber auch unter Berücksichtigung der Strichbreite), und verschieben Sie dann die Mitte des Pfads in die Mitte des Zeichenbereichs:
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);
}
}
Der Pfad füllt den Zeichenbereich aus, der beim Anzeigen im Querformat sinnvoller aussieht:
Die Pfaddaten-Cat-Seite ist ähnlich. Die Pfad- und Paint-Objekte sind beide als Felder in der PathDataCatPage
Klasse definiert:
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
};
...
}
Der Kopf einer Katze ist ein Kreis, und hier wird er mit zwei A
Befehlen gerendert, von denen jeder einen Halbkreis zeichnet. Beide A
Befehle für den Kopf definieren horizontale und vertikale Radien von 100. Der erste Bogen beginnt mit (240, 100) und endet mit (240, 300), der zum Ausgangspunkt für den zweiten Bogen wird, der auf (240, 100) endet.
Die beiden Augen werden auch mit zwei A
Befehlen gerendert, und wie beim Kopf der Katze endet der zweite A
Befehl an demselben Punkt wie der Anfang des ersten A
Befehls. Diese Befehlspaare definieren jedoch keine Auslassungspunkte A
. Das Mit jedem Bogen ist 40 Einheiten und der Radius ist auch 40 Einheiten, was bedeutet, dass diese Bögen nicht vollständig halbkreisförmig sind.
Der PaintSurface
Handler führt ähnliche Transformationen wie im vorherigen Beispiel aus, legt jedoch einen einzelnen Scale
Faktor fest, um das Seitenverhältnis beizubehalten und einen kleinen Rand bereitzustellen, sodass die Katzen-Whisker nicht die Seiten des Bildschirms berühren:
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);
}
}
Dies ist das Programm, das ausgeführt wird:
Wenn ein SKPath
Objekt normalerweise als Feld definiert ist, müssen die Konturen des Pfads im Konstruktor oder einer anderen Methode definiert werden. Bei Verwendung von SVG-Pfaddaten haben Sie jedoch gesehen, dass der Pfad vollständig in der Felddefinition angegeben werden kann.
Im vorherigen Beispiel "Hässliche Analoguhr " im Artikel "Rotate Transform " wurden die Hände der Uhr als einfache Linien angezeigt. Das nachstehende Pretty Analog Clock-Programm ersetzt diese Zeilen durch SKPath
Objekte, die als Felder in der PrettyAnalogClockPage
Klasse definiert sind, zusammen mit SKPaint
Objekten:
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
};
...
}
Die Stunden- und Minutenhändige haben jetzt bereiche eingeschlossen. Um diese Hände voneinander zu unterscheiden, werden sie mit einer schwarzen Kontur und einer grauen Füllung mit den und handFillPaint
den handStrokePaint
Objekten gezeichnet.
Im früheren Ugly Analog Clock-Beispiel wurden die kleinen Kreise, die die Stunden und Minuten markierten, in einer Schleife gezeichnet. In diesem Pretty Analog Clock-Beispiel wird ein völlig anderer Ansatz verwendet: Die Stunden- und Minutenmarkierungen werden mit den und hourMarkPaint
den minuteMarkPaint
Objekten gepunktete Linien gezeichnet:
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)
};
...
}
Im Artikel "Punkte und Striche" wurde erläutert, wie Sie die SKPathEffect.CreateDash
Methode verwenden können, um eine gestrichelte Linie zu erstellen. Das erste Argument ist ein float
Array, das im Allgemeinen zwei Elemente aufweist: Das erste Element ist die Länge der Bindestriche, und das zweite Element ist die Lücke zwischen den Bindestrichen. Wenn die StrokeCap
Eigenschaft auf SKStrokeCap.Round
festgelegt ist, werden die abgerundeten Enden des Gedankenstrichs effektiv um die Strichbreite auf beiden Seiten des Gedankenstrichs verlängert. Das Festlegen des ersten Arrayelements auf 0 erstellt daher eine gepunktete Linie.
Der Abstand zwischen diesen Punkten wird durch das zweite Arrayelement gesteuert. Wie Sie kurz sehen, werden diese beiden SKPaint
Objekte verwendet, um Kreise mit einem Radius von 90 Einheiten zu zeichnen. Der Umfang dieses Kreises ist daher 180π, was bedeutet, dass die 60-Minuten-Markierungen alle 3π Einheiten angezeigt werden müssen, was der zweite Wert in der float
Matrix minuteMarkPaint
ist. Die 12 Stundenmarken müssen alle 15π Einheiten angezeigt werden, was der Wert im zweiten float
Array ist.
Die PrettyAnalogClockPage
Klasse legt einen Timer fest, um die Oberfläche alle 16 Millisekunden ungültig zu machen, und der PaintSurface
Handler wird mit dieser Rate aufgerufen. Die früheren Definitionen von und SKPath
SKPaint
Objekten ermöglichen einen sehr sauberen Zeichnungscode:
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();
}
}
Etwas Besonderes geschieht jedoch mit der zweiten Hand. Da die Uhr alle 16 Millisekunden aktualisiert wird, kann die Millisecond
Eigenschaft des DateTime
Werts möglicherweise verwendet werden, um eine Aufräum-Second-Hand anstelle einer zu animieren, die sich in einzelnen Sprüngen von Sekunde zu Sekunde bewegt. Dieser Code lässt jedoch nicht zu, dass die Bewegung reibungslos verläuft. Stattdessen werden die Xamarin.FormsSpringIn
Beschleunigungsfunktionen und SpringOut
Animationen für eine andere Art von Bewegung verwendet. Diese Beschleunigungsfunktionen führen dazu, dass sich die zweite Hand in einer ruckerischen Weise bewegt – ein wenig zurückzuziehen, bevor es bewegt wird, und dann etwas überschießt sein Ziel, ein Effekt, der leider nicht in diesen statischen Screenshots reproduziert werden kann: