Share via


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:

Ein Beispielpfad, der mit SVG-Pfaddaten definiert ist

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, LLineToein 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 ToSvgPathDataPfaddaten 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:

Dreifacher Screenshot der Seite

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 zu Standard beizubehalten und einen kleinen Rand bereitzustellen, sodass die Whisker der Katze 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:

Dreifacher Screenshot der Seite

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 minuteMarkPaintist. 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 und SKPathSKPaint Objekte ermöglichen sehr sauber 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:

Dreifacher Screenshot der Seite