Partager via


Données de chemin SVG dans SkiaSharp

Définir des chemins à l’aide de chaînes de texte au format Graphique vectoriel évolutif

La SKPath classe prend en charge la définition d’objets de chemin d’accès entiers à partir de chaînes de texte dans un format établi par la spécification SVG (Scalable Vector Graphics). Vous verrez plus loin dans cet article comment vous pouvez représenter un chemin d’accès entier tel que celui-ci dans une chaîne de texte :

Exemple de chemin défini avec les données de chemin SVG

SVG est un langage de programmation graphique XML pour les pages web. Étant donné que SVG doit autoriser la définition des chemins dans le balisage plutôt qu’une série d’appels de fonction, la norme SVG inclut un moyen extrêmement concis de spécifier un chemin d’accès graphique entier sous forme de chaîne de texte.

Dans SkiaSharp, ce format est appelé « SVG path-data ». Le format est également pris en charge dans les environnements de programmation Windows XAML, notamment Windows Presentation Foundation et le plateforme Windows universelle, où il est appelé syntaxe de balisage de chemin d’accès ou syntaxe de déplacement et de dessin. Il peut également servir de format d’échange pour les images graphiques vectorielles, en particulier dans les fichiers texte tels que XML.

La SKPath classe définit deux méthodes avec les mots SvgPathData dans leurs noms :

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

La méthode statique ParseSvgPathData convertit une chaîne en objet SKPath , tandis qu’elle ToSvgPathData convertit un SKPath objet en chaîne.

Voici une chaîne SVG pour une étoile à cinq pointes centrée sur le point (0, 0) avec un rayon de 100 :

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

Les lettres sont des commandes qui créent un SKPath objet : M indique un MoveTo appel, L est LineToet Z doit Close fermer un contour. Chaque paire de nombres fournit une coordonnée X et Y d’un point. Notez que la L commande est suivie de plusieurs points séparés par des virgules. Dans une série de coordonnées et de points, les virgules et les espaces blancs sont traités de façon identique. Certains programmeurs préfèrent placer des virgules entre les coordonnées X et Y plutôt que entre les points, mais les virgules ou les espaces sont uniquement nécessaires pour éviter l’ambiguïté. C’est parfaitement légal :

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

La syntaxe des données de chemin SVG est officiellement documentée dans la section 8.3 de la spécification SVG. Voici un résumé :

Moveto

M x y

Cela commence un nouveau contour dans le chemin en définissant la position actuelle. Les données de chemin d’accès doivent toujours commencer par une M commande.

Lineto

L x y ...

Cette commande ajoute une ligne droite (ou des lignes) au chemin et définit la nouvelle position actuelle à la fin de la dernière ligne. Vous pouvez suivre la L commande avec plusieurs paires de coordonnées x et y .

Horizontal LineTo

H x ...

Cette commande ajoute une ligne horizontale au chemin et définit la nouvelle position actuelle à la fin de la ligne. Vous pouvez suivre la H commande avec plusieurs coordonnées x , mais cela n’a pas beaucoup de sens.

Ligne verticale

V y ...

Cette commande ajoute une ligne verticale au chemin et définit la nouvelle position actuelle à la fin de la ligne.

Close

Z

La C commande ferme le contour en ajoutant une ligne droite de la position actuelle au début du contour.

ArcTo

La commande permettant d’ajouter un arc elliptique au contour est de loin la commande la plus complexe dans l’ensemble de la spécification de données de chemin SVG. Il s’agit de la seule commande dans laquelle les nombres peuvent représenter quelque chose d’autre que les valeurs de coordonnées :

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

Les paramètres rx et ry sont les rayons horizontaux et verticaux de l’ellipse. L’angle de rotation est au niveau des aiguilles d’une montre en degrés.

Définissez le grand indicateur arc sur 1 pour le grand arc ou sur 0 pour le petit arc.

Définissez l’indicateur de balayage sur 1 pour le sens des aiguilles d’une montre et sur 0 pour le sens inverse des aiguilles d’une montre.

L’arc est dessiné au point (x, y), qui devient la nouvelle position actuelle.

CubeTo

C x1 y1 x2 y2 x3 y3 ...

Cette commande ajoute une courbe de Bézier cubique de la position actuelle à (x3, y3), qui devient la nouvelle position actuelle. Les points (x1, y1) et (x2, y2) sont des points de contrôle.

Plusieurs courbes de Bézier peuvent être spécifiées par une seule C commande. Le nombre de points doit être un multiple de 3.

Il existe également une commande de courbe de Bézier « lisse » :

S x2 y2 x3 y3 ...

Cette commande doit suivre une commande régulière de Bézier (bien que cela ne soit pas strictement obligatoire). La commande de Bézier lisse calcule le premier point de contrôle afin qu’il soit une réflexion du deuxième point de contrôle du précédent Bézier autour de leur point mutuel. Ces trois points sont donc colignes et la connexion entre les deux courbes de Bézier est lisse.

QuadTo

Q x1 y1 x2 y2 ...

Pour les courbes quadratiques de Bézier, le nombre de points doit être un multiple de 2. Le point de contrôle est (x1, y1) et le point de terminaison (et la nouvelle position actuelle) est (x2, y2)

Il existe également une commande de courbe quadratique lisse :

T x2 y2 ...

Le point de contrôle est calculé en fonction du point de contrôle de la courbe quadratique précédente.

Toutes ces commandes sont également disponibles dans les versions « relatives », où les points de coordonnées sont relatifs à la position actuelle. Ces commandes relatives commencent par des lettres minuscules, par exemple c plutôt que C pour la version relative de la commande bézier cube.

Il s’agit de l’étendue de la définition de chemin d’accès SVG. Il n’existe aucune possibilité de répéter des groupes de commandes ou d’effectuer n’importe quel type de calcul. Les commandes pour ConicTo ou les autres types de spécifications d’arc ne sont pas disponibles.

La méthode statique SKPath.ParseSvgPathData attend une chaîne valide de commandes SVG. Si une erreur de syntaxe est détectée, la méthode retourne null. Il s’agit de la seule indication d’erreur.

La ToSvgPathData méthode est pratique pour obtenir des données de chemin SVG à partir d’un objet existant SKPath à transférer vers un autre programme ou pour stocker dans un format de fichier texte tel que XML. (La ToSvgPathData méthode n’est pas illustrée dans l’exemple de code de cet article.) Ne vous attendez ToSvgPathData pas à retourner une chaîne correspondant exactement aux appels de méthode qui ont créé le chemin d’accès. En particulier, vous découvrirez que les arcs sont convertis en plusieurs QuadTo commandes, et c’est la façon dont ils apparaissent dans les données de chemin d’accès retournées par ToSvgPathData.

La page Path Data Hello indique le mot « HELLO » à l’aide des données de chemin SVG. Les objets et SKPaint les SKPath deux sont définis en tant que champs dans la 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
    };
    ...
}

Le chemin d’accès définissant la chaîne de texte commence en haut à gauche au point(0, 0). Chaque lettre est de 50 unités larges et de 100 unités de hauteur, et les lettres sont séparées par 25 unités supplémentaires, ce qui signifie que l’ensemble du chemin est de 350 unités de large.

Le « H » de « Hello » est composé de trois contours d’une ligne, tandis que le « E » est deux courbes de Bézier cubiques connectées. Notez que la C commande est suivie de six points et que deux des points de contrôle ont des coordonnées Y de –10 et 110, ce qui les place en dehors de la plage des coordonnées Y des autres lettres. Le « L » est deux lignes connectées, tandis que le « O » est un ellipse rendu avec une A commande.

Notez que la M commande qui commence le dernier contour définit la position sur le point (350, 50), qui est le centre vertical du côté gauche du « O ». Comme indiqué par les premiers nombres suivant la A commande, l’ellipse a un rayon horizontal de 25 et un rayon vertical de 50. Le point de terminaison est indiqué par la dernière paire de nombres dans la A commande, qui représente le point (300, 49,9). C’est délibérément légèrement différent du point de départ. Si le point de terminaison est défini sur le point de départ, l’arc ne sera pas rendu. Pour dessiner un ellipse complet, vous devez définir le point de terminaison proche (mais pas égal à) le point de départ, ou vous devez utiliser deux commandes ou plus A , chacune pour une partie de l’ellipse complète.

Vous pouvez ajouter l’instruction suivante au constructeur de la page, puis définir un point d’arrêt pour examiner la chaîne résultante :

string str = helloPath.ToSvgPathData();

Vous découvrirez que l’arc a été remplacé par une longue série de commandes pour une approximation fragmentaire de l’arc à l’aide de Q courbes quadratiques de Bézier.

Le PaintSurface gestionnaire obtient les limites serrées du chemin, qui n’inclut pas les points de contrôle pour les courbes « E » et « O ». Les trois transformations déplacent le centre du chemin vers le point (0, 0), mettez à l’échelle le chemin d’accès à la taille du canevas (mais aussi en tenant compte de la largeur du trait), puis déplacez le centre du chemin vers le centre du canevas :

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

Le chemin remplit le canevas, qui semble plus raisonnable lorsqu’il est consulté en mode paysage :

Capture d’écran triple de la page Path Data Hello

La page Path Data Cat est similaire. Les objets path et paint sont tous deux définis en tant que champs dans la 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 tête d’un chat est un cercle, et ici il est rendu avec deux A commandes, chacune d’entre elles dessine un demi-cercle. Les deux A commandes pour la tête définissent le rayon horizontal et vertical de 100. Le premier arc commence à (240, 100) et se termine à (240, 300), qui devient le point de départ du deuxième arc qui se termine à (240, 100).

Les deux yeux sont également rendus avec deux A commandes, et comme avec la tête du chat, la deuxième A commande se termine au même point que le début de la première A commande. Toutefois, ces paires de A commandes ne définissent pas d’ellipse. Avec chaque arc est de 40 unités et le rayon est également de 40 unités, ce qui signifie que ces arcs ne sont pas des demi-cercles complets.

Le PaintSurface gestionnaire effectue des transformations similaires comme l’exemple précédent, mais définit un facteur unique Scale pour maintenir le rapport d’aspect et fournir une petite marge afin que les moustaches du chat ne touchent pas les côtés de l’écran :

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

Voici le programme en cours d’exécution :

Capture d’écran triple de la page Path Data Cat

Normalement, lorsqu’un SKPath objet est défini en tant que champ, les contours du chemin d’accès doivent être définis dans le constructeur ou une autre méthode. Toutefois, lorsque vous utilisez des données de chemin SVG, vous avez vu que le chemin d’accès peut être spécifié entièrement dans la définition de champ.

L’exemple d’horloge analogique laid précédent dans l’article The Rotate Transform affichait les mains de l’horloge sous forme de lignes simples. Le programme Pretty Analog Clock ci-dessous remplace ces lignes par SKPath des objets définis comme des champs de la PrettyAnalogClockPage classe, ainsi SKPaint que des objets :

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

Les mains de l’heure et de la minute ont maintenant des zones fermées. Pour que ces mains soient distinctes les unes des autres, elles sont dessinées avec un contour noir et un remplissage gris à l’aide des objets et handFillPaint des handStrokePaint objets.

Dans l’exemple d’horloge analogique laid précédent, les petits cercles qui ont marqué les heures et les minutes ont été dessinées dans une boucle. Dans cet exemple d’horloge assez analogique, une approche entièrement différente est utilisée : les marques d’heure et de minute sont des lignes en pointillés dessinées avec les objets et hourMarkPaint les minuteMarkPaint objets :

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’article Points et Tirets explique comment utiliser la SKPathEffect.CreateDash méthode pour créer une ligne en pointillés. Le premier argument est un float tableau qui comporte généralement deux éléments : le premier élément est la longueur des tirets, et le deuxième élément est l’écart entre les tirets. Lorsque la StrokeCap propriété est définie SKStrokeCap.Roundsur , les extrémités arrondies du tiret allongent efficacement la longueur du tiret par la largeur du trait sur les deux côtés du tiret. Par conséquent, la définition du premier élément de tableau sur 0 crée une ligne en pointillés.

La distance entre ces points est régie par le deuxième élément de tableau. Comme vous le verrez bientôt, ces deux SKPaint objets sont utilisés pour dessiner des cercles avec un rayon de 90 unités. La circonférence de ce cercle est donc de 180π, ce qui signifie que les marques de 60 minutes doivent apparaître toutes les unités de 3π, qui est la deuxième valeur dans le float tableau en minuteMarkPaint. Les marques de 12 heures doivent apparaître toutes les 15 unités, qui est la valeur dans le deuxième float tableau.

La PrettyAnalogClockPage classe définit un minuteur pour invalider la surface toutes les 16 millisecondes, et le PaintSurface gestionnaire est appelé à ce rythme. Les définitions antérieures des SKPath objets et SKPaint des objets permettent un code de dessin très propre :

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

Quelque chose de spécial est fait avec la deuxième main, cependant. Étant donné que l’horloge est mise à jour toutes les 16 millisecondes, la Millisecond propriété de la DateTime valeur peut potentiellement être utilisée pour animer une deuxième main de balayage au lieu d’une autre qui se déplace dans des sauts discrets de la seconde à la seconde. Mais ce code n’autorise pas le mouvement à être lisse. Au lieu de cela, il utilise les Xamarin.FormsSpringIn fonctions d’accélération d’animation SpringOut pour un type de mouvement différent. Ces fonctions d’accélération provoquent le déplacement de la deuxième main de manière plus bizarre — en ressaisissant un peu avant qu’elle ne se déplace, puis légèrement sur-tir de sa destination, un effet qui malheureusement ne peut pas être reproduit dans ces captures d’écran statiques :

Capture d’écran triple de la page Pretty Analog Clock