Dati percorso SVG in SkiaSharp

Download Sample Scaricare l'esempio

Definire percorsi usando stringhe di testo nel formato Grafico vettoriale scalabile

La SKPath classe supporta la definizione di interi oggetti percorso da stringhe di testo in un formato stabilito dalla specifica SVG (Scalable Vector Graphics). Più avanti in questo articolo verrà illustrato come rappresentare un intero percorso, ad esempio questo in una stringa di testo:

A sample path defined with SVG path data

SVG è un linguaggio di programmazione grafica basato su XML per le pagine Web. Poiché SVG deve consentire la definizione dei percorsi nel markup anziché in una serie di chiamate di funzione, lo standard SVG include un modo estremamente conciso di specificare un intero percorso grafico come stringa di testo.

All'interno di SkiaSharp, questo formato viene definito "SVG path-data". Il formato è supportato anche negli ambienti di programmazione basati su XAML di Windows, tra cui Windows Presentation Foundation e il piattaforma UWP (Universal Windows Platform), dove è noto come sintassi di markup del percorso o la sintassi di spostamento e disegno dei comandi. Può anche fungere da formato di scambio per le immagini grafiche vettoriali, in particolare nei file basati su testo, ad esempio XML.

La SKPath classe definisce due metodi con le parole SvgPathData nei relativi nomi:

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

Il metodo statico ParseSvgPathData converte una stringa in un SKPath oggetto , mentre ToSvgPathData converte un SKPath oggetto in una stringa.

Ecco una stringa SVG per una stella a cinque punte centrata sul punto (0, 0) con un raggio di 100:

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

Le lettere sono comandi che compilano un SKPath oggetto: M indica una MoveTo chiamata, L è LineToe Z deve Close chiudere un contorno. Ogni coppia di numeri fornisce una coordinata X e Y di un punto. Si noti che il L comando è seguito da più punti separati da virgole. In una serie di coordinate e punti, le virgole e gli spazi vuoti vengono trattati in modo identico. Alcuni programmatori preferiscono inserire virgole tra le coordinate X e Y anziché tra i punti, ma le virgole o gli spazi sono necessari solo per evitare ambiguità. Questo è perfettamente legale:

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

La sintassi dei dati di percorso SVG è formalmente documentata nella sezione 8.3 della specifica SVG. Ecco un riepilogo:

Moveto

M x y

Viene avviato un nuovo contorno nel percorso impostando la posizione corrente. I dati del percorso devono sempre iniziare con un M comando.

LineTo

L x y ...

Questo comando aggiunge una linea retta (o linee) al percorso e imposta la nuova posizione corrente alla fine dell'ultima riga. È possibile seguire il L comando con più coppie di coordinate x e y .

Linea orizzontaleTo

H x ...

Questo comando aggiunge una riga orizzontale al percorso e imposta la nuova posizione corrente alla fine della riga. È possibile seguire il H comando con più coordinate x , ma non ha molto senso.

Linea verticale

V y ...

Questo comando aggiunge una riga verticale al percorso e imposta la nuova posizione corrente alla fine della riga.

Chiudi

Z

Il C comando chiude il contorno aggiungendo una linea retta dalla posizione corrente all'inizio del contorno.

ArcTo

Il comando per aggiungere un arco ellittico al contorno è di gran lunga il comando più complesso nell'intera specifica di dati di percorso SVG. È l'unico comando in cui i numeri possono rappresentare qualcosa di diverso dai valori di coordinate:

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

I parametri rx e ry sono i raggi orizzontali e verticali dell'ellisse. L'angolo di rotazione è in senso orario in gradi.

Impostare la bandiera grande-arco su 1 per l'arco grande o su 0 per l'arco piccolo.

Impostare il flag sweep su 1 per in senso orario e su 0 per antiorario.

L'arco viene disegnato al punto (x, y), che diventa la nuova posizione corrente.

CubicTo

C x1 y1 x2 y2 x3 y3 ...

Questo comando aggiunge una curva cubica di Bézier dalla posizione corrente a (x3, y3), che diventa la nuova posizione corrente. I punti (x1, y1) e (x2, y2) sono punti di controllo.

È possibile specificare più curve di Bézier tramite un singolo C comando. Il numero di punti deve essere un multiplo di 3.

C'è anche un comando di curva bézier "liscia":

S x2 y2 x3 y3 ...

Questo comando deve seguire un normale comando di Bézier (anche se non è strettamente richiesto). Il comando smooth Bézier calcola il primo punto di controllo in modo che sia un riflesso del secondo punto di controllo del precedente Bézier intorno al punto reciproco. Questi tre punti sono quindi colinear, e la connessione tra le due curve di Bézier è liscia.

QuadTo

Q x1 y1 x2 y2 ...

Per le curve quadratiche di Bézier, il numero di punti deve essere un multiplo di 2. Il punto di controllo è (x1, y1) e il punto finale (e la nuova posizione corrente) è (x2, y2)

È anche disponibile un comando di curva quadratica uniforme:

T x2 y2 ...

Il punto di controllo viene calcolato in base al punto di controllo della curva quadratica precedente.

Tutti questi comandi sono disponibili anche nelle versioni "relative", in cui i punti di coordinate sono relativi alla posizione corrente. Questi comandi relativi iniziano con lettere minuscole, ad esempio c anziché C per la versione relativa del comando bézier cubico.

Si tratta dell'estensione della definizione di path-data SVG. Non esiste alcuna funzionalità per i gruppi ripetuti di comandi o per l'esecuzione di qualsiasi tipo di calcolo. I comandi per ConicTo o gli altri tipi di specifiche dell'arco non sono disponibili.

Il metodo statico SKPath.ParseSvgPathData prevede una stringa valida di comandi SVG. Se viene rilevato un errore di sintassi, il metodo restituisce null. Questa è l'unica indicazione di errore.

Il ToSvgPathData metodo è utile per ottenere dati di percorso SVG da un oggetto esistente SKPath da trasferire in un altro programma o per archiviare in un formato di file basato su testo, ad esempio XML. Il ToSvgPathData metodo non è illustrato nel codice di esempio in questo articolo. Non aspettarsi di ToSvgPathData restituire una stringa corrispondente esattamente alle chiamate al metodo che ha creato il percorso. In particolare, si scoprirà che gli archi vengono convertiti in più QuadTo comandi ed è così che vengono visualizzati nei dati del percorso restituiti da ToSvgPathData.

La pagina Path Data Hello specifica la parola "HELLO" usando i dati del percorso SVG. Entrambi gli SKPath oggetti e SKPaint sono definiti come campi nella PathDataHelloPage classe :

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

Il percorso che definisce la stringa di testo inizia nell'angolo superiore sinistro del punto (0, 0). Ogni lettera è larga 50 unità e 100 unità di altezza e le lettere sono separate da un'altra 25 unità, il che significa che l'intero percorso è largo 350 unità.

La "H" di "Hello" è composta da tre contorni a una linea, mentre la "E" è due curve cubiche di Bézier collegate. Si noti che il C comando è seguito da sei punti e due dei punti di controllo hanno coordinate Y di –10 e 110, che le inserisce all'esterno dell'intervallo delle coordinate Y delle altre lettere. 'L' è costituito da due linee collegate, mentre 'O' è un'ellisse di cui viene eseguito il rendering con un A comando.

Si noti che il M comando che inizia l'ultimo contorno imposta la posizione sul punto (350, 50), ovvero il centro verticale del lato sinistro della "O". Come indicato dai primi numeri che seguono il A comando, l'ellisse ha un raggio orizzontale di 25 e un raggio verticale di 50. Il punto finale è indicato dall'ultima coppia di numeri nel A comando, che rappresenta il punto (300, 49,9). Questo è deliberatamente leggermente diverso dal punto di partenza. Se l'endpoint è impostato come uguale al punto iniziale, il rendering dell'arco non verrà eseguito. Per disegnare un'ellisse completa, è necessario impostare l'endpoint vicino (ma non uguale a) il punto iniziale oppure è necessario usare due o più A comandi, ognuno per parte dell'ellisse completa.

È possibile aggiungere l'istruzione seguente al costruttore della pagina e quindi impostare un punto di interruzione per esaminare la stringa risultante:

string str = helloPath.ToSvgPathData();

Si scoprirà che l'arco è stato sostituito con una lunga serie di Q comandi per un'approssimazione a fasi dell'arco usando curve quadratiche bézier.

Il PaintSurface gestore ottiene i limiti stretti del percorso, che non include i punti di controllo per le curve 'E' e 'O'. Le tre trasformazioni spostano il centro del percorso verso il punto (0, 0), ridimensionano il percorso alla dimensione dell'area di disegno (tenendo conto anche della larghezza del tratto) e quindi spostano il centro del percorso al centro dell'area di disegno:

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

Il percorso riempie l'area di disegno, che sembra più ragionevole quando viene visualizzata in modalità orizzontale:

Triple screenshot of the Path Data Hello page

La pagina Percorso dati cat è simile. I percorsi e gli oggetti paint sono entrambi definiti come campi nella PathDataCatPage classe :

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

La testa di un gatto è un cerchio, e qui viene eseguito il rendering con due A comandi, ognuno dei quali disegna un semicircolo. Entrambi A i comandi per la testa definiscono raggi orizzontali e verticali di 100. Il primo arco inizia a (240, 100) e termina a (240, 300), che diventa il punto iniziale per il secondo arco che termina di nuovo a (240, 100).

I due occhi vengono visualizzati anche con due A comandi, e come con la testa del gatto, il secondo A comando termina allo stesso punto dell'inizio del primo A comando. Tuttavia, queste coppie di A comandi non definiscono un'ellisse. Il con di ogni arco è di 40 unità e il raggio è anche 40 unità, il che significa che questi archi non sono interi semicircoli.

Il PaintSurface gestore esegue trasformazioni simili al campione precedente, ma imposta un singolo Scale fattore per mantenere le proporzioni e fornire un piccolo margine in modo che i whisker del gatto non tocchino i lati dello schermo:

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

Ecco il programma in esecuzione:

Triple screenshot of the Path Data Cat page

In genere, quando un SKPath oggetto viene definito come campo, i contorni del percorso devono essere definiti nel costruttore o in un altro metodo. Quando si usano i dati del percorso SVG, tuttavia, si è visto che il percorso può essere specificato interamente nella definizione del campo.

L'esempio precedente di Clock analogico brutto nell'articolo Ruota trasformazione visualizzava le mani dell'orologio come linee semplici. Il programma Pretty Analog Clock seguente sostituisce tali righe con SKPath oggetti definiti come campi nella PrettyAnalogClockPage classe insieme agli SKPaint oggetti :

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

Le mani dell'ora e del minuto ora hanno aree racchiuse. Per fare in modo che queste mani siano distinte l'una dall'altra, vengono disegnate con un contorno nero e un riempimento grigio utilizzando gli handStrokePaint oggetti e handFillPaint .

Nel precedente esempio di Clock analogico brutto , i piccoli cerchi che segnavano le ore e i minuti sono stati disegnati in un ciclo. In questo esempio di Pretty Analog Clock viene usato un approccio completamente diverso: i segni di ora e minuto sono linee tratteggiate con gli minuteMarkPaint oggetti e 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)
    };
    ...
}

L'articolo Punti e trattini ha illustrato come usare il SKPathEffect.CreateDash metodo per creare una linea tratteggiata. Il primo argomento è una float matrice che in genere ha due elementi: il primo elemento è la lunghezza dei trattini e il secondo è lo spazio tra i trattini. Quando la StrokeCap proprietà è impostata su SKStrokeCap.Round, le estremità arrotondate del trattino allungano efficacemente la lunghezza del trattino in base alla larghezza del tratto su entrambi i lati del trattino. Impostando quindi il primo elemento matrice su 0, viene creata una linea tratteggiata.

La distanza tra questi punti è governata dal secondo elemento della matrice. Come si vedrà a breve, questi due SKPaint oggetti vengono usati per disegnare cerchi con un raggio di 90 unità. La circonferenza di questo cerchio è quindi 180π, il che significa che i segni di 60 minuti devono essere visualizzati ogni 3π unità, ovvero il secondo valore nella float matrice in minuteMarkPaint. I segni di 12 ore devono essere visualizzati ogni 15 unità, ovvero il valore nella seconda float matrice.

La PrettyAnalogClockPage classe imposta un timer per invalidare la superficie ogni 16 millisecondi e il PaintSurface gestore viene chiamato a tale velocità. Le definizioni precedenti degli SKPath oggetti e SKPaint consentono codice di disegno molto pulito:

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

Qualcosa di speciale è fatto con la seconda mano, tuttavia. Poiché l'orologio viene aggiornato ogni 16 millisecondi, la Millisecond proprietà del DateTime valore può essere potenzialmente utilizzata per animare una seconda mano dello sweep anziché una che si sposta in salti discreti dal secondo al secondo. Ma questo codice non consente lo spostamento senza problemi. Usa invece le funzioni di interpolazione dell'animazione Xamarin.FormsSpringIn e SpringOut per un tipo diverso di movimento. Queste funzioni di interpolazione fanno sì che la seconda mano si muova in modo jerkier — tirando indietro un po 'prima che si sposti, e poi leggermente sovrasparendo la destinazione, un effetto che purtroppo non può essere riprodotto in questi screenshot statici:

Triple screenshot of the Pretty Analog Clock page