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 :
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 :
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 :
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 uniqueLine
avec deux pointsCubic
avec quatre pointsQuad
avec trois pointsConic
avec trois points (et appelez également laConicWeight
méthode pour le poids)Close
avec un pointDone
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 :
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 :
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.