Partager via


Effets de chemin dans SkiaSharp

Découvrez les différents effets de chemin qui permettent d’utiliser les chemins d’accès pour les caresses et le remplissage

Un effet de chemin est une instance de la SKPathEffect classe créée avec l’une des huit méthodes de création statique définies par la classe. L’objet SKPathEffect est ensuite défini sur la PathEffect propriété d’un SKPaint objet pour une variété d’effets intéressants, par exemple, en faisant un trait avec un petit chemin répliqué :

L’exemple chaîne liée

Les effets de chemin d’accès vous permettent de :

  • Traiter une ligne avec des points et des tirets
  • Trait d’une ligne avec n’importe quel chemin rempli
  • Remplir une zone avec des lignes de hachure
  • Remplir une zone avec un chemin d’accès en mosaïque
  • Faire arrondir les angles aigus
  • Ajouter une « gigue » aléatoire aux lignes et courbes

En outre, vous pouvez combiner deux effets de chemin d’accès ou plus.

Cet article montre également comment utiliser la GetFillPath méthode de SKPaint conversion d’un chemin d’accès en un autre chemin en appliquant des propriétés , SKPainty compris StrokeWidth et PathEffect. Cela entraîne des techniques intéressantes, telles que l’obtention d’un chemin d’accès qui est un contour d’un autre chemin. GetFillPath est également utile en lien avec les effets de chemin d’accès.

Points et tirets

L’utilisation de la PathEffect.CreateDash méthode a été décrite dans l’article Points et Tirets. Le premier argument de la méthode est un tableau contenant un nombre pair de deux valeurs ou plus, en alternant entre les longueurs de tirets et les longueurs d’écarts entre les tirets :

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

Ces valeurs ne sont pas relatives à la largeur du trait. Par exemple, si la largeur du trait est de 10 et que vous souhaitez une ligne composée de tirets carrés et d’écarts carrés, définissez le intervals tableau sur { 10, 10 }. L’argument phase indique où commence le modèle de tirets. Dans cet exemple, si vous souhaitez que la ligne commence par l’écart carré, définie phase sur 10.

Les extrémités des tirets sont affectées par la StrokeCap propriété de SKPaint. Pour les largeurs de trait large, il est très courant de définir cette propriété pour SKStrokeCap.Round arrondir les extrémités des tirets. Dans ce cas, les valeurs du intervals tableau n’incluent pas la longueur supplémentaire résultant de l’arrondi. Cela signifie qu’un point circulaire nécessite la spécification d’une largeur de zéro. Pour une largeur de trait de 10, pour créer une ligne avec des points circulaires et des écarts entre les points du même diamètre, utilisez un intervals tableau de { 0, 20 }.

La page Texte en pointillé animé est similaire à la page Texte plané décrite dans l’article Intégration de texte et de graphiques dans lequel il affiche des caractères de texte en plan en définissant la Style propriété de l’objet SKPaintSKPaintStyle.Strokesur . En outre, le texte en pointillé animé utilise SKPathEffect.CreateDash pour donner à ce plan une apparence en pointillés, et le programme anime également l’argument phase de la SKPathEffect.CreateDash méthode pour que les points semblent voyager autour des caractères de texte. Voici la page en mode paysage :

Capture d’écran triple de la page Texte en pointillé animé

La AnimatedDottedTextPage classe commence par définir certaines constantes, et remplace également les méthodes et OnDisappearing les OnAppearing méthodes de l’animation :

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

Le PaintSurface gestionnaire commence par créer un SKPaint objet pour afficher le texte. La TextSize propriété est ajustée en fonction de la largeur de l’écran :

public class AnimatedDottedTextPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.MeasureText(text, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

Vers la fin de la méthode, la SKPathEffect.CreateDash méthode est appelée à l’aide du dashArray champ défini comme champ et de la valeur animée phase . L’instance SKPathEffect est définie sur la PathEffect propriété de l’objet SKPaint pour afficher le texte.

Vous pouvez également définir l’objet SKPathEffect sur l’objet SKPaint avant de mesurer le texte et de le centrer sur la page. Toutefois, dans ce cas, les points animés et les tirets provoquent une certaine variation de la taille du texte rendu, et le texte a tendance à vibrer un peu. (Essayez-le !)

Vous remarquerez également que comme les points animés entourent les caractères de texte, il y a un certain point dans chaque courbe fermée où les points semblent apparaître et sortir de l’existence. C’est là que le chemin qui définit le contour du caractère commence et se termine. Si la longueur du chemin n’est pas un multiple intégral de la longueur du motif de tiret (dans ce cas 20 pixels), seule une partie de ce modèle peut s’adapter à la fin du chemin.

Il est possible d’ajuster la longueur du modèle de tiret pour qu’il corresponde à la longueur du chemin d’accès, mais cela nécessite de déterminer la longueur du chemin, une technique couverte dans l’article Informations sur le chemin d’accès et énumération.

Le programme Dot / Dash Morph anime le modèle de tiret lui-même afin que les tirets semblent se diviser en points, qui combinent pour former à nouveau des tirets :

Capture d’écran triple de la page Dot Dash Morph

La DotDashMorphPage classe remplace les méthodes et OnDisappearing les OnAppearing méthodes comme le programme précédent, mais la classe définit l’objet SKPaint comme un champ :

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

Le PaintSurface gestionnaire crée un chemin d’accès elliptique basé sur la taille de la page et exécute une longue section de code qui définit les variables et phase les dashArray variables. Comme la variable t animée varie de 0 à 1, les if blocs se décomposent cette fois en quatre trimestres, et dans chacun de ces trimestres, tsub s’étend également de 0 à 1. À la fin, le programme crée et SKPathEffect le définit sur l’objet pour le SKPaint dessin.

Du chemin d’accès au chemin d’accès

La GetFillPath méthode de transformer un chemin d’accès SKPaint en un autre en fonction des paramètres de l’objet SKPaint . Pour voir comment cela fonctionne, remplacez l’appel canvas.DrawPath dans le programme précédent par le code suivant :

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Dans ce nouveau code, l’appel GetFillPath convertit le ellipsePath (qui est juste un ovale) en newPath, qui est ensuite affiché avec newPaint. L’objet newPaint est créé avec tous les paramètres de propriété par défaut, sauf que la Style propriété est définie en fonction de la valeur de retour booléenne à partir de GetFillPath.

Les visuels sont identiques à l’exception de la couleur, qui est définie mais ellipsePaint pas newPaint. Au lieu de la simple ellipse définie dans ellipsePath, newPath contient de nombreux contours de chemin qui définissent la série de points et de tirets. Il s’agit du résultat de l’application de différentes propriétés de ellipsePaint (en particulier, StrokeWidth, StrokeCapet PathEffect) à ellipsePath et de placer le chemin résultant dans newPath. La GetFillPath méthode retourne une valeur booléenne indiquant si le chemin de destination doit être rempli ou non ; dans cet exemple, la valeur de retour est true destinée à remplir le chemin d’accès.

Essayez de modifier le Style paramètre SKPaintStyle.StrokenewPaint dans et vous verrez les contours de chemin d’accès individuels décrits avec une ligne de largeur d’un pixel.

Stroking with a Path

La SKPathEffect.Create1DPath méthode est conceptuellement similaire à celle SKPathEffect.CreateDash que vous spécifiez un chemin plutôt qu’un modèle de tirets et d’écarts. Ce chemin est répliqué plusieurs fois pour traiter la ligne ou la courbe.

La syntaxe est :

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

En général, le chemin auquel vous passez Create1DPath sera petit et centré autour du point (0, 0). Le advance paramètre indique la distance entre les centres du chemin à mesure que le chemin est répliqué sur la ligne. Vous définissez généralement cet argument sur la largeur approximative du chemin d’accès. L’argument phase joue le même rôle ici que dans la CreateDash méthode.

Les SKPath1DPathEffectStyle trois membres sont les suivants :

  • Translate
  • Rotate
  • Morph

Le Translate membre provoque le maintien du chemin dans la même orientation qu’il est répliqué le long d’une ligne ou d’une courbe. Pour Rotate, le chemin est pivoté en fonction d’une tangente à la courbe. Le chemin a son orientation normale pour les lignes horizontales. Morph est similaire à ce Rotate que le chemin lui-même soit également courbé pour correspondre à la courbure de la ligne en cours de trait.

La page Effet de chemin d’accès 1D illustre ces trois options. Le fichier OneDimensionalPathEffectPage.xaml définit un sélecteur contenant trois éléments correspondant aux trois membres de l’énumération :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

Le fichier OneDimensionalPathEffectPage.xaml.cs code-behind définit trois SKPathEffect objets en tant que champs. Ils sont tous créés à SKPath l’aide d’objets SKPathEffect.Create1DPath créés à l’aide SKPath.ParseSvgPathDatade . La première est une boîte simple, la seconde est une forme de diamant, et la troisième est un rectangle. Ils sont utilisés pour illustrer les trois styles d’effet :

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath path = new SKPath())
        {
            path.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

            canvas.DrawPath(path, pathPaint);
        }
    }
}

Le PaintSurface gestionnaire crée une courbe de Bézier qui se boucle autour de lui-même et accède au sélecteur pour déterminer qui PathEffect doit être utilisé pour le traiter. Les trois options ( Translate, Rotateet Morph — sont affichées de gauche à droite :

Capture d’écran triple de la page Effet de chemin d’accès 1D

Le chemin spécifié dans la SKPathEffect.Create1DPath méthode est toujours rempli. Le chemin d’accès spécifié dans la DrawPath méthode est toujours tracé si l’objet SKPaint a sa PathEffect propriété définie sur un effet de chemin d’accès 1D. Notez que l’objet pathPaint n’a aucun Style paramètre, qui est normalement défini par défaut Fill, mais que le chemin d’accès est tracé indépendamment.

La zone utilisée dans l’exemple Translate est carrée de 20 pixels et l’argument advance est défini sur 24. Cette différence provoque un écart entre les cases lorsque la ligne est à peu près horizontale ou verticale, mais les cases se chevauchent un peu lorsque la ligne est diagonale, car la diagonale de la boîte est de 28,3 pixels.

La forme de diamant dans l’exemple Rotate est également de 20 pixels de large. La advance valeur est définie sur 20 afin que les points continuent à toucher à mesure que le diamant est pivoté avec la courbure de la ligne.

La forme de rectangle dans l’exemple Morph est de 50 pixels de large avec un advance paramètre de 55 pour faire un petit écart entre les rectangles, car ils sont plié autour de la courbe de Bézier.

Si l’argument advance est inférieur à la taille du chemin, les chemins répliqués peuvent se chevaucher. Cela peut entraîner des effets intéressants. La page Chaîne liée affiche une série de cercles qui se chevauchent qui semblent ressembler à une chaîne liée, qui se bloque à la forme distinctive d’un catétenaire :

Capture d’écran triple de la page Chaîne liée

Regardez très près et vous verrez que ce ne sont pas réellement des cercles. Chaque lien de la chaîne est deux arcs, dimensionnés et positionnés afin qu’ils semblent se connecter avec des liens adjacents.

Une chaîne ou un câble de distribution uniforme de poids se bloque sous la forme d’un catenaire. Une arche construite sous la forme d’un catenaire inversé bénéficie d’une distribution égale de la pression du poids d’une arche. Le catenaire a une description mathématique apparemment simple :

y = a · cosh(x / a)

La cosh est la fonction cosinus hyperbolique. Pour x égal à 0, cosh est égal à zéro et y est égal à a. C’est le centre du catenaire. Comme la fonction cosinus , cosh est dit même, ce qui signifie que cosh(–x) est égal à cosh(x), et les valeurs augmentent pour augmenter les arguments positifs ou négatifs. Ces valeurs décrivent les courbes qui forment les côtés du catenaire.

La recherche de la valeur appropriée d’un pour ajuster le catenaire aux dimensions de la page du téléphone n’est pas un calcul direct. Si w et h sont la largeur et la hauteur d’un rectangle, la valeur optimale d’un correspond à l’équation suivante :

cosh(w / 2 / a) = 1 + h / a

La méthode suivante de la LinkedChainPage classe incorpore cette égalité en faisant référence aux deux expressions situées à gauche et à droite du signe égal comme left et right. Pour les petites valeurs d’un, est supérieure rightà ; pour les valeurs importantes d’un, left est inférieure à right. left La while boucle se limite à une valeur optimale d’un :

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

L’objet SKPath des liens est créé dans le constructeur de la classe, et l’objet résultant SKPathEffect est ensuite défini sur la PathEffect propriété de l’objet SKPaint stocké en tant que champ :

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

Le travail principal du PaintSurface gestionnaire consiste à créer un chemin d’accès pour le catenaire lui-même. Après avoir déterminé l’optimal un et le stockant dans la optA variable, il doit également calculer un décalage à partir du haut de la fenêtre. Ensuite, il peut accumuler une collection de SKPoint valeurs pour le catenaire, le transformer en chemin d’accès et dessiner le chemin avec l’objet créé SKPaint précédemment :

public class LinkedChainPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

Ce programme définit le chemin utilisé pour Create1DPath avoir son point (0, 0) dans le centre. Cela semble raisonnable, car le point (0, 0) du chemin est aligné avec la ligne ou la courbe qu’il orne. Toutefois, vous pouvez utiliser un point non centré (0, 0) pour certains effets spéciaux.

La page Tapis roulant crée un chemin semblable à un tapis roulant oblong avec un haut et un bas courbés dimensionnés aux dimensions de la fenêtre. Ce chemin est tracé avec un objet simple SKPaint de 20 pixels de large et gris coloré, puis tracé à nouveau avec un autre SKPaint objet avec un SKPathEffect objet faisant référence à un chemin semblable à un petit compartiment :

Capture d’écran triple de la page Tapis roulant

Le point (0, 0) du chemin de compartiment est la poignée, donc lorsque l’argument phase est animé, les compartiments semblent tourner autour de la ceinture de tapis roulant, peut-être scopage de l’eau au fond et le décharger au sommet.

La ConveyorBeltPage classe implémente l’animation avec des remplacements des méthodes et OnDisappearing des OnAppearing remplacements. Le chemin d’accès du compartiment est défini dans le constructeur de la page :

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

Le code de création de compartiment se termine par deux transformations qui rendent le compartiment un peu plus grand et le tournent de côté. L’application de ces transformations était plus facile que d’ajuster toutes les coordonnées dans le code précédent.

Le PaintSurface gestionnaire commence par définir un chemin pour le tapis roulant lui-même. Il s’agit simplement d’une paire de lignes et d’une paire de demi-cercles dessinés avec une ligne gris foncé de 20 pixels :

public class ConveyorBeltPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

La logique de dessin du tapis roulant ne fonctionne pas en mode paysage.

Les compartiments doivent être espacés d’environ 200 pixels à part sur le tapis roulant. Toutefois, le tapis roulant n’est probablement pas un multiple de 200 pixels de long, ce qui signifie que, comme phase l’argument d’est SKPathEffect.Create1DPath animé, les compartiments s’affichent dans et hors de l’existence.

Pour cette raison, le programme calcule d’abord une valeur nommée length qui correspond à la longueur du tapis roulant. Comme le tapis roulant se compose de lignes droites et de demi-cercles, il s’agit d’un calcul simple. Ensuite, le nombre de compartiments est calculé en divisant length par 200. Il est arrondi à l’entier le plus proche, et ce nombre est ensuite divisé en length. Le résultat est un espacement pour un nombre intégral de compartiments. L’argument phase est simplement une fraction de cela.

À partir du chemin d’accès au chemin d’accès à nouveau

En bas du gestionnaire dans DrawSurfacele tapis roulant, commentez l’appel et remplacez-le canvas.DrawPath par le code suivant :

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Comme avec l’exemple précédent de GetFillPath, vous verrez que les résultats sont identiques à l’exception de la couleur. Après l’exécution GetFillPath, l’objet newPath contient plusieurs copies du chemin du compartiment, chacune positionnée au même endroit que l’animation les a positionnées au moment de l’appel.

Hachage d’une zone

La SKPathEffect.Create2DLines méthode remplit une zone avec des lignes parallèles, souvent appelées lignes de hache. La méthode a la syntaxe suivante :

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

L’argument width spécifie la largeur du trait des lignes de hachure. Le matrix paramètre est une combinaison de mise à l’échelle et de rotation facultative. Le facteur de mise à l’échelle indique l’incrément de pixels que Skia utilise pour espacer les lignes de hachure. La séparation entre les lignes est le facteur de mise à l’échelle moins l’argument width . Si le facteur de mise à l’échelle est inférieur ou égal à la width valeur, il n’y aura pas d’espace entre les lignes de hache et la zone semble remplie. Spécifiez la même valeur pour la mise à l’échelle horizontale et verticale.

Par défaut, les lignes de hachure sont horizontales. Si le paramètre contient une matrix rotation, les lignes de hache sont pivotées dans le sens des aiguilles d’une montre.

La page Remplissage de hachure illustre cet effet de chemin d’accès. La HatchFillPage classe définit trois effets de chemin en tant que champs, le premier pour les lignes de hachures horizontales avec une largeur de 3 pixels avec un facteur de mise à l’échelle indiquant qu’ils sont espacés de 6 pixels. La séparation entre les lignes est donc de trois pixels. Le deuxième effet de chemin est destiné aux lignes de hachures verticales avec une largeur de six pixels espacés de 24 pixels (par conséquent, la séparation est de 18 pixels) et le troisième est destiné aux lignes de hachures diagonales de 12 pixels espacés de 36 pixels.

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Notez la méthode de matrice Multiply . Étant donné que les facteurs de mise à l’échelle horizontale et verticale sont identiques, l’ordre dans lequel les matrices de mise à l’échelle et de rotation sont multipliées n’a pas d’importance.

Le PaintSurface gestionnaire utilise ces trois effets de chemin avec trois couleurs différentes en combinaison avec fillPaint pour remplir un rectangle arrondi dimensionné pour s’adapter à la page. La Style propriété définie est fillPaint ignorée ; lorsque l’objet SKPaint inclut un effet de chemin d’accès créé à partir SKPathEffect.Create2DLinede , la zone est remplie indépendamment des éléments suivants :

public class HatchFillPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

Si vous examinez attentivement les résultats, vous verrez que les lignes de hachure rouge et bleue ne sont pas limitées précisément au rectangle arrondi. (Il s’agit apparemment d’une caractéristique du code Skia sous-jacent.) Si cela n’est pas satisfaisant, une autre approche s’affiche pour les lignes de hache diagonales en vert : le rectangle arrondi est utilisé comme chemin de découpage et les lignes de hachure sont dessinées sur toute la page.

Le PaintSurface gestionnaire se termine par un appel pour simplement traiter le rectangle arrondi, afin de voir l’écart avec les lignes de hachure rouge et bleue :

Capture d’écran triple de la page Remplissage de hachure

L’écran Android ne ressemble pas vraiment à ceci : la mise à l’échelle de la capture d’écran a entraîné la consolidation des lignes rouges minces et des espaces minces dans des lignes rouges apparemment plus larges et des espaces plus larges.

Remplissage avec un chemin

Vous SKPathEffect.Create2DPath pouvez remplir une zone avec un chemin d’accès répliqué horizontalement et verticalement, en mosaïque en effet la zone :

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

Les SKMatrix facteurs de mise à l’échelle indiquent l’espacement horizontal et vertical du chemin répliqué. Mais vous ne pouvez pas faire pivoter le chemin à l’aide de cet matrix argument ; si vous souhaitez que le chemin pivote, faites pivoter le chemin lui-même à l’aide de la Transform méthode définie par SKPath.

Le chemin répliqué est normalement aligné sur les bords gauche et supérieur de l’écran plutôt que sur la zone remplie. Vous pouvez remplacer ce comportement en fournissant des facteurs de traduction entre 0 et les facteurs de mise à l’échelle pour spécifier des décalages horizontaux et verticaux à partir des côtés gauche et supérieur.

La page Remplissage des vignettes de chemin d’accès illustre cet effet de chemin d’accès. Le chemin utilisé pour la mosaïne de la zone est défini en tant que champ dans la PathTileFillPage classe. Les coordonnées horizontales et verticales vont de –40 à 40, ce qui signifie que ce chemin est de 80 pixels carrés :

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

Dans le PaintSurface gestionnaire, les SKPathEffect.Create2DPath appels définissent l’espacement horizontal et vertical sur 64 pour que les vignettes carrées de 80 pixels se chevauchent. Heureusement, le chemin ressemble à une pièce de puzzle, maillage agréable avec des mosaïques adjacentes :

Capture d’écran triple de la page Remplissage des vignettes chemin d’accès

La mise à l’échelle à partir de la capture d’écran d’origine entraîne une certaine distorsion, en particulier sur l’écran Android.

Notez que ces vignettes apparaissent toujours entières et ne sont jamais tronquées. Sur les deux premières captures d’écran, il n’est même pas évident que la zone remplie est un rectangle arrondi. Si vous souhaitez tronquer ces vignettes dans une zone particulière, utilisez un chemin de découpage.

Essayez de définir la Style propriété de l’objet SKPaintStrokesur , et vous verrez les vignettes individuelles décrites plutôt que remplies.

Il est également possible de remplir une zone avec une bitmap en mosaïque, comme illustré dans l’article SkiaSharp bitmap en mosaïque.

Arrondi d’angles aigus

Le programme Heptagon arrondi présenté dans l’article Three Ways to Draw an Arc a utilisé un arc tangent pour courber les points d’une figure à sept côtés. La page Heptagon arrondie montre une approche beaucoup plus facile qui utilise un effet de chemin créé à partir de la SKPathEffect.CreateCorner méthode :

public static SKPathEffect CreateCorner (Single radius)

Bien que l’argument unique soit nommé radius, vous devez le définir sur la moitié du rayon d’angle souhaité. (Il s’agit d’une caractéristique du code Skia sous-jacent.)

Voici le PaintSurface gestionnaire dans la AnotherRoundedHeptagonPage classe :

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = new SKPoint[numVertices];
    double vertexAngle = -0.5f * Math.PI;       // straight up

    // Coordinates of the vertices of the polygon
    for (int vertex = 0; vertex < numVertices; vertex++)
    {
        vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                       radius * (float)Math.Sin(vertexAngle));
        vertexAngle += 2 * Math.PI / numVertices;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // Render the path in the center of the screen
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = 10;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

Vous pouvez utiliser cet effet avec des traits ou des remplissages basés sur la Style propriété de l’objet SKPaint . Ici, il est en cours d’exécution :

Capture d’écran triple de la page Un autre heptagon arrondi

Vous verrez que cet heptagon arrondi est identique au programme précédent. Si vous avez besoin de plus convaincant que le rayon d’angle est vraiment 100 plutôt que le 50 spécifié dans l’appel SKPathEffect.CreateCorner , vous pouvez annuler les commentaires de l’instruction finale dans le programme et voir un cercle de 100 rayons superposé sur le coin.

Gigue aléatoire

Parfois, les lignes droites parfaites des graphiques informatiques ne sont pas tout à fait ce que vous voulez, et un peu aléatoire est souhaitée. Dans ce cas, vous souhaiterez essayer la SKPathEffect.CreateDiscrete méthode :

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

Vous pouvez utiliser cet effet de chemin d’accès pour le remplissage ou le remplissage. Les lignes sont séparées en segments connectés ( la longueur approximative spécifiée par segLength ) et s’étendent dans différentes directions. L’étendue de l’écart par rapport à la ligne d’origine est spécifiée par deviation.

L’argument final est une valeur initiale utilisée pour générer la séquence pseudo-aléatoire utilisée pour l’effet. L’effet de gigue sera un peu différent pour différentes graines. L’argument a une valeur par défaut de zéro, ce qui signifie que l’effet est le même chaque fois que vous exécutez le programme. Si vous souhaitez une gigue différente chaque fois que l’écran est repeint, vous pouvez définir la valeur initiale sur la Millisecond propriété d’une DataTime.Now valeur (par exemple).

La page Jitter Experiment vous permet d’expérimenter différentes valeurs dans le cadre d’un rectangle :

Capture d’écran triple de la page JitterExperiment

Le programme est simple. Le fichier JitterExperimentPage.xaml instancie deux Slider éléments et un SKCanvasView:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Le PaintSurface gestionnaire dans le fichier code-behind JitterExperimentPage.xaml.cs est appelé chaque fois qu’une Slider valeur change. Il appelle SKPathEffect.CreateDiscrete à l’aide des deux Slider valeurs et utilise celui-ci pour traiter un rectangle :

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

Vous pouvez également utiliser cet effet pour le remplissage, auquel cas le contour de la zone remplie est soumis à ces écarts aléatoires. La page Texte gigue illustre l’utilisation de cet effet de chemin d’accès pour afficher du texte. La plupart du code dans le PaintSurface gestionnaire de la JitterTextPage classe est consacrée au dimensionnement et au centre du texte :

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);

        // Calculate offsets to center the text on the screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Ici, il s’exécute en mode paysage :

Triple capture d’écran de la page JitterText

Plan du chemin d’accès

Vous avez déjà vu deux petits exemples de la GetFillPath méthode de SKPaint, qui existe deux versions :

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

Seuls les deux premiers arguments sont requis. La méthode accède au chemin référencé par l’argument src , modifie les données de chemin d’accès en fonction des propriétés de trait dans l’objet SKPaint (y compris la PathEffect propriété), puis écrit les résultats dans le dst chemin. Le resScale paramètre permet de réduire la précision de créer un chemin de destination plus petit, et l’argument cullRect peut éliminer les contours en dehors d’un rectangle.

Une utilisation de base de cette méthode n’implique pas d’effets de chemin d’accès du tout : si l’objet SKPaint a sa Style propriété définie SKPaintStyle.Strokesur , et n’a pas son PathEffect jeu, crée GetFillPath un chemin qui représente un plan du chemin source comme s’il avait été traité par les propriétés de peinture.

Par exemple, si le src chemin est un cercle simple de rayon 500 et que l’objet SKPaint spécifie une largeur de trait de 100, le dst chemin devient deux cercles concentriques, un avec un rayon de 450 et l’autre avec un rayon de 550. La méthode est appelée GetFillPath , car le remplissage de ce dst chemin est identique à celui src du chemin d’accès. Mais vous pouvez également traiter le dst chemin pour voir les contours du chemin.

L’option Appuyer pour décrire le chemin d’accès illustre cela. Le SKCanvasView fichier TapGestureRecognizer TapToOutlineThePathPage.xaml est instancié et instancié. Le fichier code-behind TapToOutlineThePathPage.xaml.cs définit trois SKPaint objets en tant que champs, deux pour la largeur de trait de 100 et 20, et la troisième pour le remplissage :

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

Si l’écran n’a pas été tapé, le PaintSurface gestionnaire utilise les blueFill objets et redThickStroke peints pour afficher un chemin circulaire :

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

Le cercle est rempli et traité comme prévu :

Capture d’écran triple de la page Appuyer pour décrire le chemin d’accès normal

Lorsque vous appuyez sur l’écran, outlineThePath est défini truesur , et le PaintSurface gestionnaire crée un objet frais SKPath et l’utilise comme chemin de destination dans un appel à GetFillPath l’objet redThickStroke de peinture. Ce chemin de destination est ensuite rempli et tracé avec redThinStroke, ce qui aboutit à ce qui suit :

Capture d’écran triple de l’appui sur Le plan de la page Chemin d’accès

Les deux cercles rouges indiquent clairement que le chemin circulaire d’origine a été converti en deux contours circulaires.

Cette méthode peut être très utile dans le développement de chemins à utiliser pour la SKPathEffect.Create1DPath méthode. Les chemins que vous spécifiez dans ces méthodes sont toujours remplis lorsque les chemins sont répliqués. Si vous ne souhaitez pas que l’intégralité du chemin soit remplie, vous devez définir soigneusement les contours.

Par exemple, dans l’exemple Chaîne liée, les liens ont été définis avec une série de quatre arcs, dont chaque paire était basée sur deux rayons pour décrire la zone du chemin à remplir. Il est possible de remplacer le code dans la LinkedChainPage classe pour le faire un peu différemment.

Tout d’abord, vous devez redéfinir la linkRadius constante :

const float linkRadius = 27.5f;
const float linkThickness = 5;

Il linkPath ne s’agit maintenant que de deux arcs basés sur ce rayon unique, avec les angles de début et les angles de balayage souhaités :

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

L’objet outlinePath est ensuite le destinataire du contour du linkPath moment où il est tracé avec les propriétés spécifiées dans strokePaint.

Un autre exemple d’utilisation de cette technique est à venir pour le chemin d’accès utilisé dans une méthode.

Combinaison d’effets de chemin d’accès

Les deux dernières méthodes de création statique sont SKPathEffectSKPathEffect.CreateSum et SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Ces deux méthodes combinent deux effets de chemin d’accès pour créer un effet de chemin composite. La CreateSum méthode crée un effet de chemin similaire aux deux effets de chemin appliqués séparément, tout en CreateCompose appliquant un effet de chemin (le inner) puis à outer celui-ci.

Vous avez déjà vu comment la méthode de SKPaint peut convertir un chemin vers un autre chemin en fonction SKPaint des propriétés (y comprisPathEffect) afin qu’il ne soit pas trop mystérieux comment un SKPaint objet peut effectuer cette opération deux fois avec les deux effets de chemin spécifiés dans les CreateSum méthodes ouCreateCompose.GetFillPath

L’une des utilisations CreateSum évidentes consiste à définir un SKPaint objet qui remplit un chemin d’accès avec un effet de chemin d’accès et le trait avec un autre effet de chemin. Ceci est illustré dans l’exemple Chats dans Frame , qui affiche un tableau de chats dans un cadre avec des bords scallopés :

Capture d’écran triple de la page Chats dans le cadre

La CatsInFramePage classe commence par définir plusieurs champs. Vous pouvez reconnaître le premier champ de la PathDataCatPage classe à partir de l’article SVG Path Data . Le deuxième chemin est basé sur une ligne et un arc pour le modèle de pécallope du cadre :

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    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 catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

Peut catPath être utilisé dans la SKPathEffect.Create2DPath méthode si la SKPaint propriété d’objet Style est définie sur Stroke. Toutefois, si le catPath produit est utilisé directement dans ce programme, la tête entière du chat sera remplie, et les moustaches ne seront même pas visibles. (Essayez-le !) Il est nécessaire d’obtenir le plan de ce chemin et d’utiliser ce plan dans la SKPathEffect.Create2DPath méthode.

Le constructeur effectue ce travail. Il applique d’abord deux transformations pour catPath déplacer le point (0, 0) vers le centre et le réduire en taille. GetFillPath obtient tous les contours des contours dans outlinedCatPath, et cet objet est utilisé dans l’appel SKPathEffect.Create2DPath . Les facteurs de mise à l’échelle de la SKMatrix valeur sont légèrement plus grands que la taille horizontale et verticale du chat pour fournir un peu de mémoire tampon entre les vignettes, tandis que les facteurs de traduction ont été dérivés quelque peu empiriquement afin qu’un chat complet soit visible dans le coin supérieur gauche du cadre :

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

Le constructeur appelle SKPathEffect.Create1DPath ensuite le cadre pécallope. Notez que la largeur du chemin est de 100 pixels, mais que l’avance est de 75 pixels afin que le chemin répliqué se chevauche autour du cadre. Instruction finale des appels SKPathEffect.CreateSum du constructeur pour combiner les deux effets de chemin d’accès et définir le résultat sur l’objet SKPaint .

Tout ce travail permet au PaintSurface gestionnaire d’être assez simple. Il doit uniquement définir un rectangle et le dessiner à l’aide framePaintde :

public class CatsInFramePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

Les algorithmes derrière les effets du chemin entraînent toujours l’affichage du chemin d’accès entier utilisé pour l’affichage ou le remplissage, ce qui peut entraîner l’affichage de certains visuels en dehors du rectangle. L’appel ClipRect avant l’appel DrawRect permet aux visuels d’être considérablement propre er. (Essayez-le sans découpage !)

Il est courant d’utiliser SKPathEffect.CreateCompose pour ajouter une gigue à un autre effet de chemin. Vous pouvez certainement expérimenter par vous-même, mais voici un exemple un peu différent :

Les lignes de hachures pointillées remplissent un ellipse avec des lignes de hachures qui sont en pointillés. La plupart du travail de la DashedHatchLinesPage classe est effectué directement dans les définitions de champ. Ces champs définissent un effet de tiret et un effet de hachure. Ils sont définis comme static étant donné qu’ils sont ensuite référencés dans un SKPathEffect.CreateCompose appel dans la SKPaint définition :

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Le PaintSurface gestionnaire doit contenir uniquement la surcharge standard plus un appel à DrawOval:

public class DashedHatchLinesPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

Comme vous l’avez déjà découvert, les lignes de hache ne sont pas précisément limitées à l’intérieur de la zone, et dans cet exemple, elles commencent toujours à gauche avec un tiret entier :

Capture d’écran triple de la page Lignes de hachures pointillées

Maintenant que vous avez vu des effets de chemin qui vont des points simples et des tirets à des combinaisons étranges, utilisez votre imagination et voyez ce que vous pouvez créer.