Partager via


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 :

Singe à 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 :

Capture d’écran triple de la page Monkey through Keyhole

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 SKRectSKPath 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 :

Capture d’écran triple de la page Four Circle Intersect Clip

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 existante

  • Intersect 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.Differencepar , vous verrez les éléments suivants :

Capture d’écran triple de la page Four Circle Intersect Clip avec opération de différence

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 :

Capture d’écran triple de la page Opérations clip

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 :

Capture d’écran triple de la page Opérations de région

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 DifferenceIntersectopé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 :

Capture d’écran triple de la page Paint région

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 :

Triple capture d’écran de la page De trèfle à quatre feuilles