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é :
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 , SKPaint
y 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 SKPaint
SKPaintStyle.Stroke
sur . 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 :
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 :
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
, StrokeCap
et 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.Stroke
newPaint
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.ParseSvgPathData
de . 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
, Rotate
et Morph
— sont affichées de gauche à droite :
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 :
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 :
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 DrawSurface
le 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.Create2DLine
de , 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 :
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 :
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 SKPaint
Stroke
sur , 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 :
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 :
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 :
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.Stroke
sur , 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 :
Lorsque vous appuyez sur l’écran, outlineThePath
est défini true
sur , 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 :
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 SKPathEffect
SKPathEffect.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 :
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 framePaint
de :
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 :
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.