Partager via


Informations et énumération de tracés

Obtenir des informations sur les chemins d’accès et énumérer le contenu

La SKPath classe définit plusieurs propriétés et méthodes qui vous permettent d’obtenir des informations sur le chemin d’accès. Les Bounds propriétés (et TightBounds méthodes associées) obtiennent les dimensions métriques d’un chemin d’accès. La Contains méthode vous permet de déterminer si un point particulier se trouve dans un chemin d’accès.

Il est parfois utile de déterminer la longueur totale de toutes les lignes et courbes qui constituent un chemin. Le calcul de cette longueur n’est pas une tâche algorithmiquement simple, donc une classe entière nommée PathMeasure est consacrée à celle-ci.

Il est également parfois utile d’obtenir toutes les opérations de dessin et points qui constituent un chemin d’accès. Au début, cette installation peut sembler inutile : si votre programme a créé le chemin d’accès, le programme connaît déjà le contenu. Toutefois, vous avez vu que les chemins d’accès peuvent également être créés par des effets de chemin d’accès et en convertissant des chaînes de texte en chemins d’accès. Vous pouvez également obtenir toutes les opérations de dessin et points qui composent ces chemins. Une possibilité est d’appliquer une transformation algorithmique à tous les points, par exemple, pour encapsuler du texte autour d’un hémisphère :

Texte encapsulé dans un hémisphère

Obtention de la longueur du chemin d’accès

Dans l’article Chemins d’accès et texte, vous avez vu comment utiliser la méthode pour dessiner une chaîne de texte dont la DrawTextOnPath ligne de base suit le cours d’un chemin d’accès. Mais que se passe-t-il si vous voulez dimensionner le texte pour qu’il corresponde précisément au chemin ? Le dessin de texte autour d’un cercle est facile, car la circonférence d’un cercle est simple à calculer. Mais la circonférence d’un ellipse ou de la longueur d’une courbe de Bézier n’est pas si simple.

La SKPathMeasure classe peut vous aider. Le constructeur accepte un SKPath argument et la Length propriété révèle sa longueur.

Cette classe est illustrée dans l’exemple Path Length , qui est basé sur la page Courbe de Bezier. Le fichier PathLengthPage.xaml dérive et InteractivePage inclut une interface tactile :

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
                       Title="Path Length">
    <Grid BackgroundColor="White">
        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface" />
        <Grid.Effects>
            <tt:TouchEffect Capture="True"
                            TouchAction="OnTouchEffectAction" />
        </Grid.Effects>
    </Grid>
</local:InteractivePage>

Le fichier code-behind PathLengthPage.xaml.cs vous permet de déplacer quatre points tactiles pour définir les points de terminaison et les points de contrôle d’une courbe de Bézier cube. Trois champs définissent une chaîne de texte, un SKPaint objet et une largeur calculée du texte :

public partial class PathLengthPage : InteractivePage
{
    const string text = "Compute length of path";

    static SKPaint textPaint = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Black,
        TextSize = 10,
    };

    static readonly float baseTextWidth = textPaint.MeasureText(text);
    ...
}

Le baseTextWidth champ est la largeur du texte en fonction d’un TextSize paramètre de 10.

Le PaintSurface gestionnaire dessine la courbe de Bézier, puis dimensionne le texte en fonction de sa longueur entière :

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

    canvas.Clear();

    // Draw path with cubic Bezier curve
    using (SKPath path = new SKPath())
    {
        path.MoveTo(touchPoints[0].Center);
        path.CubicTo(touchPoints[1].Center,
                     touchPoints[2].Center,
                     touchPoints[3].Center);

        canvas.DrawPath(path, strokePaint);

        // Get path length
        SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);

        // Find new text size
        textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;

        // Draw text on path
        canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
    }
    ...
}

La Length propriété de l’objet nouvellement créé SKPathMeasure obtient la longueur du chemin d’accès. La longueur du chemin d’accès est divisée par la baseTextWidth valeur (qui est la largeur du texte en fonction d’une taille de texte de 10), puis multipliée par la taille de texte de base de 10. Le résultat est une nouvelle taille de texte pour afficher le texte le long de ce chemin :

Capture d’écran triple de la page Longueur du chemin d’accès

Lorsque la courbe de Bézier devient plus longue ou plus courte, vous pouvez voir la taille du texte changer.

Parcourir le chemin d’accès

SKPathMeasure peut faire plus que de mesurer la longueur du chemin. Pour toute valeur comprise entre zéro et la longueur du chemin, un SKPathMeasure objet peut obtenir la position sur le chemin et la tangente à la courbe de chemin à ce stade. La tangente est disponible sous la forme d’un vecteur sous la forme d’un SKPoint objet, ou sous forme de rotation encapsulée dans un SKMatrix objet. Voici les méthodes d’obtention de SKPathMeasure ces informations de différentes manières et flexibles :

Boolean GetPosition (Single distance, out SKPoint position)

Boolean GetTangent (Single distance, out SKPoint tangent)

Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)

Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)

Les membres de l’énumération SKPathMeasureMatrixFlags sont les suivants :

  • GetPosition
  • GetTangent
  • GetPositionAndTangent

La page Unicycle Half-Pipe anime une figure de bâton sur un unicycle qui semble monter en arrière le long d’une courbe de Bézier cubique :

Capture d’écran triple de la page Monocycle Half-Pipe

L’objet SKPaint utilisé pour caresser à la fois le demi-canal et le unicycle est défini comme un champ dans la UnicycleHalfPipePage classe. Est également défini l’objet SKPath du monocycle :

public class UnicycleHalfPipePage : ContentPage
{
    ...
    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };

    SKPath unicyclePath = SKPath.ParseSvgPathData(
        "M 0 0" +
        "A 25 25 0 0 0 0 -50" +
        "A 25 25 0 0 0 0 0 Z" +
        "M 0 -25 L 0 -100" +
        "A 15 15 0 0 0 0 -130" +
        "A 15 15 0 0 0 0 -100 Z" +
        "M -25 -85 L 25 -85");
    ...
}

La classe contient les remplacements standard des méthodes et OnDisappearing des OnAppearing méthodes d’animation. Le PaintSurface gestionnaire crée le chemin d’accès pour le demi-canal, puis le dessine. Un SKPathMeasure objet est ensuite créé en fonction de ce chemin :

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

        canvas.Clear();

        using (SKPath pipePath = new SKPath())
        {
            pipePath.MoveTo(50, 50);
            pipePath.CubicTo(0, 1.25f * info.Height,
                             info.Width - 0, 1.25f * info.Height,
                             info.Width - 50, 50);

            canvas.DrawPath(pipePath, strokePaint);

            using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
            {
                float length = pathMeasure.Length;

                // Animate t from 0 to 1 every three seconds
                TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
                float t = (float)(timeSpan.TotalSeconds % 5 / 5);

                // t from 0 to 1 to 0 but slower at beginning and end
                t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);

                SKMatrix matrix;
                pathMeasure.GetMatrix(t * length, out matrix,
                                      SKPathMeasureMatrixFlags.GetPositionAndTangent);

                canvas.SetMatrix(matrix);
                canvas.DrawPath(unicyclePath, strokePaint);
            }
        }
    }
}

Le PaintSurface gestionnaire calcule une valeur t comprise entre 0 et 1 toutes les cinq secondes. Il utilise ensuite la Math.Cos fonction pour convertir cela en valeur comprise t entre 0 et 1 et 0, où 0 correspond au unicycle au début à gauche, tandis que 1 correspond au unicycle en haut à droite. La fonction cosinus provoque la vitesse la plus lente au sommet du canal et la plus rapide au bas.

Notez que cette valeur t doit être multipliée par la longueur du chemin d’accès pour le premier argument à GetMatrix. La matrice est ensuite appliquée à l’objet SKCanvas pour dessiner le chemin d’accès unicycle.

Énumération du chemin d’accès

Deux classes incorporées vous SKPath permettent d’énumérer le contenu du chemin d’accès. Ces classes sont SKPath.Iterator et SKPath.RawIterator. Les deux classes sont très similaires, mais SKPath.Iterator peuvent éliminer les éléments du chemin avec une longueur nulle, ou près d’une longueur nulle. Il RawIterator est utilisé dans l’exemple ci-dessous.

Vous pouvez obtenir un objet de type SKPath.RawIterator en appelant la CreateRawIterator méthode de SKPath. L’énumération à travers le chemin d’accès est effectuée en appelant à plusieurs reprises la Next méthode. Passez-lui un tableau de quatre SKPoint valeurs :

SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);

La Next méthode retourne un membre du type d’énumération SKPathVerb . Ces valeurs indiquent la commande de dessin particulière dans le chemin d’accès. Le nombre de points valides insérés dans le tableau dépend de ce verbe :

  • Move avec un point unique
  • Line avec deux points
  • Cubic avec quatre points
  • Quad avec trois points
  • Conic avec trois points (et appelez également la ConicWeight méthode pour le poids)
  • Close avec un point
  • Done

Le Done verbe indique que l’énumération du chemin d’accès est terminée.

Notez qu’il n’y a pas Arc de verbes. Cela indique que tous les arcs sont convertis en courbes de Bézier lorsqu’ils sont ajoutés au chemin.

Certaines informations du SKPoint tableau sont redondantes. Par exemple, si un Move verbe est suivi d’un Line verbe, le premier des deux points qui accompagnent le Line verbe est le même que le Move point. Dans la pratique, cette redondance est très utile. Lorsque vous obtenez un Cubic verbe, il est accompagné par les quatre points qui définissent la courbe de Bézier cubique. Vous n’avez pas besoin de conserver la position actuelle établie par le verbe précédent.

Toutefois, le verbe problématique est Close. Cette commande dessine une ligne droite de la position actuelle au début du contour établi précédemment par la Move commande. Dans l’idéal, le Close verbe doit fournir ces deux points plutôt qu’un seul point. Ce qui est pire, c’est que le point qui accompagne le Close verbe est toujours (0, 0). Lorsque vous énumérez un chemin, vous devrez probablement conserver le Move point et la position actuelle.

Énumération, aplatissement et malformation

Il est parfois souhaitable d’appliquer une transformation algorithmique à un chemin d’accès pour le malformer d’une certaine façon :

Texte encapsulé dans un hémisphère

La plupart de ces lettres se composent de lignes droites, mais ces lignes droites ont apparemment été tordues en courbes. Comment est-ce possible ?

La clé est que les lignes droites originales sont divisées en une série de lignes droites plus petites. Ces lignes droites plus petites peuvent ensuite être manipulées de différentes façons pour former une courbe.

Pour faciliter ce processus, l’exemple contient une classe statique PathExtensions avec une Interpolate méthode qui décompose une ligne droite en de nombreuses lignes courtes qui ne sont qu’une seule unité de longueur. En outre, la classe contient plusieurs méthodes qui convertissent les trois types de courbes de Bézier en une série de minuscules lignes droites qui se rapprochent de la courbe. (Les formules paramétriques ont été présentées dans l’article Trois types de courbes de Bézier.) Ce processus est appelé aplatissement de la courbe :

static class PathExtensions
{
    ...
    static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * pt0.X + t * pt1.X;
            float y = (1 - t) * pt0.Y + t * pt1.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
                        3 * t * (1 - t) * (1 - t) * pt1.X +
                        3 * t * t * (1 - t) * pt2.X +
                        t * t * t * pt3.X;
            float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
                        3 * t * (1 - t) * (1 - t) * pt1.Y +
                        3 * t * t * (1 - t) * pt2.Y +
                        t * t * t * pt3.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
    {
        int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
        SKPoint[] points = new SKPoint[count];

        for (int i = 0; i < count; i++)
        {
            float t = (i + 1f) / count;
            float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
            float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
            float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
            x /= denominator;
            y /= denominator;
            points[i] = new SKPoint(x, y);
        }

        return points;
    }

    static double Length(SKPoint pt0, SKPoint pt1)
    {
        return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
    }
}

Toutes ces méthodes sont référencées à partir de la méthode CloneWithTransform d’extension également incluse dans cette classe et indiquées ci-dessous. Cette méthode clone un chemin d’accès en énumérant les commandes de chemin d’accès et en construisant un nouveau chemin en fonction des données. Toutefois, le nouveau chemin d’accès se compose uniquement des appels et LineTo des MoveTo appels. Toutes les courbes et lignes droites sont réduites à une série de minuscules lignes.

Lors de l’appel CloneWithTransform, vous passez à la méthode a Func<SKPoint, SKPoint>, qui est une fonction avec un SKPaint paramètre qui retourne une SKPoint valeur. Cette fonction est appelée pour chaque point pour appliquer une transformation algorithmique personnalisée :

static class PathExtensions
{
    public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
    {
        SKPath pathOut = new SKPath();

        using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
        {
            SKPoint[] points = new SKPoint[4];
            SKPathVerb pathVerb = SKPathVerb.Move;
            SKPoint firstPoint = new SKPoint();
            SKPoint lastPoint = new SKPoint();

            while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
            {
                switch (pathVerb)
                {
                    case SKPathVerb.Move:
                        pathOut.MoveTo(transform(points[0]));
                        firstPoint = lastPoint = points[0];
                        break;

                    case SKPathVerb.Line:
                        SKPoint[] linePoints = Interpolate(points[0], points[1]);

                        foreach (SKPoint pt in linePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[1];
                        break;

                    case SKPathVerb.Cubic:
                        SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);

                        foreach (SKPoint pt in cubicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[3];
                        break;

                    case SKPathVerb.Quad:
                        SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);

                        foreach (SKPoint pt in quadPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Conic:
                        SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());

                        foreach (SKPoint pt in conicPoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        lastPoint = points[2];
                        break;

                    case SKPathVerb.Close:
                        SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);

                        foreach (SKPoint pt in closePoints)
                        {
                            pathOut.LineTo(transform(pt));
                        }

                        firstPoint = lastPoint = new SKPoint(0, 0);
                        pathOut.Close();
                        break;
                }
            }
        }
        return pathOut;
    }
    ...
}

Étant donné que le chemin cloné est réduit à de minuscules lignes droites, la fonction de transformation a la capacité de convertir des lignes droites en courbes.

Notez que la méthode conserve le premier point de chaque contour dans la variable appelée firstPoint et la position actuelle après chaque commande de dessin de la variable lastPoint. Ces variables sont nécessaires pour construire la ligne fermante finale lorsqu’un Close verbe est rencontré.

L’exemple GlobularText utilise cette méthode d’extension pour encapsuler du texte autour d’un hémisphère dans un effet 3D :

Capture d’écran triple de la page Texte globulaire

Le GlobularTextPage constructeur de classe effectue cette transformation. Il crée un SKPaint objet pour le texte, puis obtient un SKPath objet à partir de la GetTextPath méthode. Il s’agit du chemin passé à la CloneWithTransform méthode d’extension avec une fonction de transformation :

public class GlobularTextPage : ContentPage
{
    SKPath globePath;

    public GlobularTextPage()
    {
        Title = "Globular Text";

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

        using (SKPaint textPaint = new SKPaint())
        {
            textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
            textPaint.TextSize = 100;

            using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
            {
                SKRect textPathBounds;
                textPath.GetBounds(out textPathBounds);

                globePath = textPath.CloneWithTransform((SKPoint pt) =>
                {
                    double longitude = (Math.PI / textPathBounds.Width) *
                                            (pt.X - textPathBounds.Left) - Math.PI / 2;
                    double latitude = (Math.PI / textPathBounds.Height) *
                                            (pt.Y - textPathBounds.Top) - Math.PI / 2;

                    longitude *= 0.75;
                    latitude *= 0.75;

                    float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
                    float y = (float)Math.Sin(latitude);

                    return new SKPoint(x, y);
                });
            }
        }
    }
    ...
}

La fonction de transformation calcule d’abord deux valeurs nommées longitude et latitude cette plage comprise entre –π/2 en haut et à gauche du texte, à π/2 à droite et en bas du texte. La plage de ces valeurs n’est pas visuellement satisfaisante, de sorte qu’elles sont réduites en multipliant par 0,75. (Essayez le code sans ces ajustements. Le texte devient trop obscur aux pôles nord et sud, et trop mince sur les côtés.) Ces coordonnées sphériques tridimensionnelles sont converties en coordonnées à deux dimensions x et y par formules standard.

Le nouveau chemin d’accès est stocké en tant que champ. Le PaintSurface gestionnaire doit ensuite simplement centrer et mettre à l’échelle le chemin d’accès pour l’afficher à l’écran :

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

        canvas.Clear();

        using (SKPaint pathPaint = new SKPaint())
        {
            pathPaint.Style = SKPaintStyle.Fill;
            pathPaint.Color = SKColors.Blue;
            pathPaint.StrokeWidth = 3;
            pathPaint.IsAntialias = true;

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.Scale(0.45f * Math.Min(info.Width, info.Height));     // radius
            canvas.DrawPath(globePath, pathPaint);
        }
    }
}

C’est une technique très polyvalente. Si le tableau d’effets de chemin décrit dans l’article Effets de chemin d’accès n’englobe pas tout à fait quelque chose que vous avez ressenti devrait être inclus, il s’agit d’un moyen de combler les lacunes.