Détourage avec tracés et régions
Utiliser des chemins d’accès pour découper des graphiques vers des zones spécifiques et pour créer des régions
Il est parfois nécessaire de restreindre le rendu des graphiques à une zone particulière. C’est ce qu’on appelle le découpage. Vous pouvez utiliser le découpage pour des effets spéciaux, tels que cette image d’un singe vu à travers un trou de clés :
La zone de découpage est la zone de l’écran dans laquelle les graphiques sont rendus. Tout ce qui est affiché en dehors de la zone de découpage n’est pas rendu. La zone de découpage est généralement définie par un rectangle ou un SKPath
objet, mais vous pouvez également définir une zone de découpage à l’aide d’un SKRegion
objet. Ces deux types d’objets semblent d’abord liés, car vous pouvez créer une région à partir d’un chemin d’accès. Toutefois, vous ne pouvez pas créer un chemin d’accès à partir d’une région, et ils sont très différents en interne : un chemin comprend une série de lignes et de courbes, tandis qu’une région est définie par une série de lignes d’analyse horizontales.
L’image ci-dessus a été créée par la page Monkey through Keyhole . La MonkeyThroughKeyholePage
classe définit un chemin à l’aide de données SVG et utilise le constructeur pour charger une bitmap à partir des ressources du programme :
public class MonkeyThroughKeyholePage : ContentPage
{
SKBitmap bitmap;
SKPath keyholePath = SKPath.ParseSvgPathData(
"M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130 Z");
public MonkeyThroughKeyholePage()
{
Title = "Monkey through Keyhole";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
...
}
Bien que l’objet keyholePath
décrit le contour d’un trou de clés, les coordonnées sont complètement arbitraires et reflètent ce qui était pratique lorsque les données de chemin d’accès ont été conçues. Pour cette raison, le PaintSurface
gestionnaire obtient les limites de ce chemin et appelle Translate
et Scale
déplace le chemin vers le centre de l’écran et le rend presque aussi haut que l’écran :
public class MonkeyThroughKeyholePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Set transform to center and enlarge clip path to window height
SKRect bounds;
keyholePath.GetTightBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.98f * info.Height / bounds.Height);
canvas.Translate(-bounds.MidX, -bounds.MidY);
// Set the clip path
canvas.ClipPath(keyholePath);
// Reset transforms
canvas.ResetMatrix();
// Display monkey to fill height of window but maintain aspect ratio
canvas.DrawBitmap(bitmap,
new SKRect((info.Width - info.Height) / 2, 0,
(info.Width + info.Height) / 2, info.Height));
}
}
Mais le chemin n’est pas rendu. Au lieu de cela, en suivant les transformations, le chemin d’accès est utilisé pour définir une zone de découpage avec cette instruction :
canvas.ClipPath(keyholePath);
Le PaintSurface
gestionnaire réinitialise ensuite les transformations avec un appel et ResetMatrix
dessine la bitmap pour s’étendre à la hauteur totale de l’écran. Ce code suppose que la bitmap est carrée, que cette bitmap particulière est. La bitmap est rendue uniquement dans la zone définie par le chemin de découpage :
Le chemin de découpage est soumis aux transformations en vigueur lorsque la ClipPath
méthode est appelée, et non aux transformations en vigueur lorsqu’un objet graphique (tel qu’une bitmap) est affiché. Le chemin de découpage fait partie de l’état de canevas enregistré avec la Save
méthode et restauré avec la Restore
méthode.
Combinaison de chemins de découpage
Strictement parlant, la zone de découpage n’est pas « définie » par la ClipPath
méthode. Au lieu de cela, il est combiné avec le chemin de découpage existant, qui commence comme un rectangle égal à la taille du canevas. Vous pouvez obtenir les limites rectangulaires de la zone de découpage à l’aide de la LocalClipBounds
propriété ou de la DeviceClipBounds
propriété. La LocalClipBounds
propriété retourne une SKRect
valeur qui reflète toutes les transformations qui peuvent être en vigueur. La DeviceClipBounds
propriété retourne une RectI
valeur. Il s’agit d’un rectangle avec des dimensions entières et décrit la zone de découpage en dimensions de pixels réelles.
Tout appel pour ClipPath
réduire la zone de découpage en combinant la zone de découpage avec une nouvelle zone. Syntaxe complète de la ClipPath
méthode qui combine la zone de découpage avec un rectangle :
public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);
Par défaut, la zone de découpage résultante est une intersection de la zone de découpage existante et de celle SKRect
SKPath
spécifiée dans la ou ClipRect
la ClipPath
méthode. Ceci est illustré dans la page Four Circle Intersect Clip . Le PaintSurface
gestionnaire de la FourCircleInteresectClipPage
classe réutilise le même SKPath
objet pour créer quatre cercles qui se chevauchent, chacun réduisant la zone de découpage par le biais d’appels successifs à ClipPath
:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float size = Math.Min(info.Width, info.Height);
float radius = 0.4f * size;
float offset = size / 2 - radius;
// Translate to center
canvas.Translate(info.Width / 2, info.Height / 2);
using (SKPath path = new SKPath())
{
path.AddCircle(-offset, -offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(-offset, offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(offset, -offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(offset, offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Fill;
paint.Color = SKColors.Blue;
canvas.DrawPaint(paint);
}
}
}
C’est l’intersection de ces quatre cercles :
L’énumération SKClipOperation
n’a que deux membres :
Difference
supprime le chemin d’accès ou le rectangle spécifié de la zone de découpage existanteIntersect
croise le chemin d’accès ou le rectangle spécifié avec la zone de découpage existante
Si vous remplacez les quatre SKClipOperation.Intersect
arguments de la FourCircleIntersectClipPage
classe SKClipOperation.Difference
par , vous verrez les éléments suivants :
Quatre cercles qui se chevauchent ont été retirés de la zone de découpage.
La page Clip Operations illustre la différence entre ces deux opérations avec seulement une paire de cercles. Le premier cercle à gauche est ajouté à la zone de découpage avec l’opération clip par défaut de Intersect
, tandis que le deuxième cercle à droite est ajouté à la zone de découpage avec l’opération clip indiquée par l’étiquette de texte :
La ClipOperationsPage
classe définit deux SKPaint
objets comme des champs, puis divise l’écran en deux zones rectangulaires. Ces zones sont différentes selon que le téléphone est en mode portrait ou paysage. La DisplayClipOp
classe affiche ensuite le texte et les appels ClipPath
avec les deux chemins de cercle pour illustrer chaque opération de clip :
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float x = 0;
float y = 0;
foreach (SKClipOperation clipOp in Enum.GetValues(typeof(SKClipOperation)))
{
// Portrait mode
if (info.Height > info.Width)
{
DisplayClipOp(canvas, new SKRect(x, y, x + info.Width, y + info.Height / 2), clipOp);
y += info.Height / 2;
}
// Landscape mode
else
{
DisplayClipOp(canvas, new SKRect(x, y, x + info.Width / 2, y + info.Height), clipOp);
x += info.Width / 2;
}
}
}
void DisplayClipOp(SKCanvas canvas, SKRect rect, SKClipOperation clipOp)
{
float textSize = textPaint.TextSize;
canvas.DrawText(clipOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
rect.Top += textSize;
float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
float xCenter = rect.MidX;
float yCenter = rect.MidY;
canvas.Save();
using (SKPath path1 = new SKPath())
{
path1.AddCircle(xCenter - radius / 2, yCenter, radius);
canvas.ClipPath(path1);
using (SKPath path2 = new SKPath())
{
path2.AddCircle(xCenter + radius / 2, yCenter, radius);
canvas.ClipPath(path2, clipOp);
canvas.DrawPaint(fillPaint);
}
}
canvas.Restore();
}
L’appel DrawPaint
entraîne normalement le remplissage de l’intégralité du canevas avec cet SKPaint
objet, mais dans ce cas, la méthode peint simplement dans la zone de découpage.
Exploration des régions
Vous pouvez également définir une zone de découpage en termes d’objet SKRegion
.
Un objet nouvellement créé SKRegion
décrit une zone vide. En règle générale, le premier appel sur l’objet est SetRect
de sorte que la région décrit une zone rectangulaire. Le paramètre à SetRect
utiliser est une SKRectI
valeur : un rectangle avec des coordonnées entières, car il spécifie le rectangle en termes de pixels. Vous pouvez ensuite appeler SetPath
avec un SKPath
objet. Cela crée une région identique à l’intérieur du chemin d’accès, mais clippée à la région rectangulaire initiale.
La région peut également être modifiée en appelant l’une des surcharges de Op
méthode, comme celle-ci :
public Boolean Op(SKRegion region, SKRegionOperation op)
L’énumération SKRegionOperation
est similaire à celle-ci SKClipOperation
, mais elle a plus de membres :
Difference
Intersect
Union
XOR
ReverseDifference
Replace
La région sur laquelle vous effectuez l’appel Op
est combinée à la région spécifiée en tant que paramètre basé sur le SKRegionOperation
membre. Lorsque vous obtenez enfin une région adaptée à la capture, vous pouvez définir celle-ci comme zone de découpage du canevas à l’aide de la ClipRegion
méthode de SKCanvas
:
public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)
La capture d’écran suivante montre les zones de découpage basées sur les six opérations de région. Le cercle gauche est la région sur laquelle la Op
méthode est appelée, et le cercle droit est la région passée à la Op
méthode :
Ces deux cercles sont-ils tous les possibilités de combiner ces deux cercles ? Considérez l’image résultante comme une combinaison de trois composants, qui sont vus par eux-mêmes dans les opérations et ReverseDifference
les Difference
Intersect
opérations. Le nombre total de combinaisons est de deux à la troisième puissance, soit huit. Les deux qui sont manquantes sont la région d’origine (qui résulte de ne pas appeler Op
du tout) et d’une région entièrement vide.
Il est plus difficile d’utiliser des régions pour le découpage, car vous devez d’abord créer un chemin, puis une région à partir de ce chemin, puis combiner plusieurs régions. La structure globale de la page Opérations de région est très similaire aux opérations clip, mais la RegionOperationsPage
classe divise l’écran en six zones et affiche le travail supplémentaire nécessaire pour utiliser les régions pour ce travail :
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float x = 0;
float y = 0;
float width = info.Height > info.Width ? info.Width / 2 : info.Width / 3;
float height = info.Height > info.Width ? info.Height / 3 : info.Height / 2;
foreach (SKRegionOperation regionOp in Enum.GetValues(typeof(SKRegionOperation)))
{
DisplayClipOp(canvas, new SKRect(x, y, x + width, y + height), regionOp);
if ((x += width) >= info.Width)
{
x = 0;
y += height;
}
}
}
void DisplayClipOp(SKCanvas canvas, SKRect rect, SKRegionOperation regionOp)
{
float textSize = textPaint.TextSize;
canvas.DrawText(regionOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
rect.Top += textSize;
float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
float xCenter = rect.MidX;
float yCenter = rect.MidY;
SKRectI recti = new SKRectI((int)rect.Left, (int)rect.Top,
(int)rect.Right, (int)rect.Bottom);
using (SKRegion wholeRectRegion = new SKRegion())
{
wholeRectRegion.SetRect(recti);
using (SKRegion region1 = new SKRegion(wholeRectRegion))
using (SKRegion region2 = new SKRegion(wholeRectRegion))
{
using (SKPath path1 = new SKPath())
{
path1.AddCircle(xCenter - radius / 2, yCenter, radius);
region1.SetPath(path1);
}
using (SKPath path2 = new SKPath())
{
path2.AddCircle(xCenter + radius / 2, yCenter, radius);
region2.SetPath(path2);
}
region1.Op(region2, regionOp);
canvas.Save();
canvas.ClipRegion(region1);
canvas.DrawPaint(fillPaint);
canvas.Restore();
}
}
}
Voici une grande différence entre la ClipPath
méthode et la ClipRegion
méthode :
Important
Contrairement à la ClipPath
méthode, la ClipRegion
méthode n’est pas affectée par les transformations.
Pour comprendre la raison de cette différence, il est utile de comprendre ce qu’est une région. Si vous avez pensé à la façon dont les opérations de clip ou de région peuvent être implémentées en interne, il semble probablement très compliqué. Plusieurs chemins potentiellement très complexes sont combinés, et le contour du chemin résultant est probablement un cauchemar algorithmique.
Ce travail est simplifié considérablement si chaque chemin est réduit à une série de lignes d’analyse horizontales, telles que celles dans les téléviseurs à tube à vide vieux mode. Chaque ligne d’analyse est simplement une ligne horizontale avec un point de départ et un point de terminaison. Par exemple, un cercle avec un rayon de 10 pixels peut être décomposé en 20 lignes d’analyse horizontale, chacune commençant à gauche du cercle et se termine à la partie droite. La combinaison de deux cercles avec n’importe quelle opération de région devient très simple, car il s’agit simplement d’examiner les coordonnées de début et de fin de chaque paire de lignes d’analyse correspondantes.
C’est ce qu’est une région : série de lignes d’analyse horizontale qui définissent une zone.
Toutefois, lorsqu’une zone est réduite à une série de lignes d’analyse, ces lignes d’analyse sont basées sur une dimension de pixel particulière. Strictement parlant, la région n’est pas un objet graphique vectoriel. Il est plus proche de la nature d’une bitmap monochrome compressée que d’un chemin d’accès. Par conséquent, les régions ne peuvent pas être mises à l’échelle ou pivotées sans perdre de fidélité et, pour cette raison, elles ne sont pas transformées lorsqu’elles sont utilisées pour les zones de découpage.
Toutefois, vous pouvez appliquer des transformations à des régions à des fins de peinture. Le programme Region Paint illustre de manière vive la nature interne des régions. La RegionPaintPage
classe crée un SKRegion
objet basé sur un SKPath
cercle de rayons de 10 unités. Une transformation développe ensuite ce cercle pour remplir la page :
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int radius = 10;
// Create circular path
using (SKPath circlePath = new SKPath())
{
circlePath.AddCircle(0, 0, radius);
// Create circular region
using (SKRegion circleRegion = new SKRegion())
{
circleRegion.SetRect(new SKRectI(-radius, -radius, radius, radius));
circleRegion.SetPath(circlePath);
// Set transform to move it to center and scale up
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(Math.Min(info.Width / 2, info.Height / 2) / radius);
// Fill region
using (SKPaint fillPaint = new SKPaint())
{
fillPaint.Style = SKPaintStyle.Fill;
fillPaint.Color = SKColors.Orange;
canvas.DrawRegion(circleRegion, fillPaint);
}
// Stroke path for comparison
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Style = SKPaintStyle.Stroke;
strokePaint.Color = SKColors.Blue;
strokePaint.StrokeWidth = 0.1f;
canvas.DrawPath(circlePath, strokePaint);
}
}
}
}
L’appel DrawRegion
remplit la région en orange, tandis que l’appel DrawPath
a trait le chemin d’accès d’origine en bleu pour la comparaison :
La région est clairement une série de coordonnées discrètes.
Si vous n’avez pas besoin d’utiliser des transformations en lien avec vos zones de découpage, vous pouvez utiliser des régions pour le découpage, comme le montre la page de trèfle à quatre feuilles. La FourLeafCloverPage
classe construit une région composite à partir de quatre régions circulaires, définit cette région composite comme zone de découpage, puis dessine une série de 360 lignes droites émanant du centre de la page :
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float xCenter = info.Width / 2;
float yCenter = info.Height / 2;
float radius = 0.24f * Math.Min(info.Width, info.Height);
using (SKRegion wholeScreenRegion = new SKRegion())
{
wholeScreenRegion.SetRect(new SKRectI(0, 0, info.Width, info.Height));
using (SKRegion leftRegion = new SKRegion(wholeScreenRegion))
using (SKRegion rightRegion = new SKRegion(wholeScreenRegion))
using (SKRegion topRegion = new SKRegion(wholeScreenRegion))
using (SKRegion bottomRegion = new SKRegion(wholeScreenRegion))
{
using (SKPath circlePath = new SKPath())
{
// Make basic circle path
circlePath.AddCircle(xCenter, yCenter, radius);
// Left leaf
circlePath.Transform(SKMatrix.MakeTranslation(-radius, 0));
leftRegion.SetPath(circlePath);
// Right leaf
circlePath.Transform(SKMatrix.MakeTranslation(2 * radius, 0));
rightRegion.SetPath(circlePath);
// Make union of right with left
leftRegion.Op(rightRegion, SKRegionOperation.Union);
// Top leaf
circlePath.Transform(SKMatrix.MakeTranslation(-radius, -radius));
topRegion.SetPath(circlePath);
// Combine with bottom leaf
circlePath.Transform(SKMatrix.MakeTranslation(0, 2 * radius));
bottomRegion.SetPath(circlePath);
// Make union of top with bottom
bottomRegion.Op(topRegion, SKRegionOperation.Union);
// Exclusive-OR left and right with top and bottom
leftRegion.Op(bottomRegion, SKRegionOperation.XOR);
// Set that as clip region
canvas.ClipRegion(leftRegion);
// Set transform for drawing lines from center
canvas.Translate(xCenter, yCenter);
// Draw 360 lines
for (double angle = 0; angle < 360; angle++)
{
float x = 2 * radius * (float)Math.Cos(Math.PI * angle / 180);
float y = 2 * radius * (float)Math.Sin(Math.PI * angle / 180);
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Color = SKColors.Green;
strokePaint.StrokeWidth = 2;
canvas.DrawLine(0, 0, x, y, strokePaint);
}
}
}
}
}
}
Il ne ressemble pas vraiment à un clover à quatre feuilles, mais il s’agit d’une image qui peut être difficile à restituer sans découpage :