Freigeben über


Schneiden mit Pfaden und Regionen

Verwenden von Pfaden zum Ausschneiden von Grafiken zu bestimmten Bereichen und zum Erstellen von Regionen

Es ist manchmal erforderlich, das Rendern von Grafiken auf einen bestimmten Bereich einzuschränken. Dies wird als Clipping bezeichnet. Sie können Clipping für Spezialeffekte verwenden, z. B. dieses Bild eines Affen, der durch ein Schlüsselloch gesehen wird:

Affen durch ein Schlüsselloch

Der Clippingbereich ist der Bereich des Bildschirms, in dem Grafiken gerendert werden. Alles, was außerhalb des Beschneidungsbereichs angezeigt wird, wird nicht gerendert. Der Clippingbereich wird in der Regel durch ein Rechteck oder ein SKPath Objekt definiert, Sie können aber auch einen Clippingbereich mithilfe eines SKRegion Objekts definieren. Diese beiden Objekttypen scheinen zunächst miteinander in Beziehung zu stehen, da Sie einen Bereich aus einem Pfad erstellen können. Sie können jedoch keinen Pfad aus einer Region erstellen und sind intern sehr unterschiedlich: Ein Pfad besteht aus einer Reihe von Linien und Kurven, während ein Bereich durch eine Reihe horizontaler Scanlinien definiert wird.

Das bild oben wurde von der Monkey through Keyhole page erstellt. Die MonkeyThroughKeyholePage Klasse definiert einen Pfad mithilfe von SVG-Daten und verwendet den Konstruktor, um eine Bitmap aus Programmressourcen zu laden:

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);
        }
    }
    ...
}

Obwohl das keyholePath Objekt die Kontur eines Schlüssellochs beschreibt, sind die Koordinaten völlig willkürlich und spiegeln wider, was bei der Entwicklung der Pfaddaten praktisch war. Aus diesem Grund ruft der PaintSurface Handler die Grenzen dieses Pfads und Aufrufe Translate ab und Scale bewegt den Pfad in die Mitte des Bildschirms und macht ihn fast so hoch wie der Bildschirm:

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));
    }
}

Der Pfad wird jedoch nicht gerendert. Stattdessen wird der Pfad nach den Transformationen verwendet, um einen Clippingbereich mit dieser Anweisung festzulegen:

canvas.ClipPath(keyholePath);

Der PaintSurface Handler setzt dann die Transformationen mit einem Aufruf ResetMatrix zurück und zeichnet die Bitmap, um auf die volle Höhe des Bildschirms zu erweitern. Dieser Code geht davon aus, dass die Bitmap quadratisch ist, was diese bestimmte Bitmap ist. Die Bitmap wird nur innerhalb des bereichs gerendert, der durch den Clippingpfad definiert wird:

Dreifacher Screenshot der Seite

Der Clippingpfad unterliegt den Transformationen, die wirksam werden, wenn die ClipPath Methode aufgerufen wird, und nicht den Transformationen, die wirksam werden, wenn ein grafisches Objekt (z. B. eine Bitmap) angezeigt wird. Der Clippingpfad ist Teil des Canvaszustands, der mit der Save Methode gespeichert und mit der Restore Methode wiederhergestellt wird.

Kombinieren von Clippingpfaden

Streng genommen wird der Beschneidungsbereich nicht durch die ClipPath Methode "festgelegt". Stattdessen wird er mit dem vorhandenen Clippingpfad kombiniert, der als Rechteck mit der Größe des Zeichenbereichs beginnt. Sie können die rechteckigen Begrenzungen des Beschneidungsbereichs mithilfe LocalClipBounds der Eigenschaft oder der DeviceClipBounds Eigenschaft abrufen. Die LocalClipBounds Eigenschaft gibt einen SKRect Wert zurück, der alle Transformationen widerspiegelt, die wirksam sein können. Die DeviceClipBounds Eigenschaft gibt einen RectI Wert zurück. Dies ist ein Rechteck mit ganzzahligen Abmessungen und beschreibt den Beschneidungsbereich in tatsächlichen Pixelabmessungen.

Jeder Aufruf, um den Beschneidungsbereich zu ClipPath reduzieren, indem der Clippingbereich mit einem neuen Bereich kombiniert wird. Die vollständige Syntax der ClipPath Methode, die den Clippingbereich mit einem Rechteck kombiniert:

public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);

Standardmäßig ist der resultierende Beschneidungsbereich eine Schnittmenge des vorhandenen Beschneidungsbereichs und des oder der SKPathSKRect in der ClipPath Oder-Methode ClipRect angegeben. Dies wird auf der Seite "Four Circles Intersect Clip " veranschaulicht. Der PaintSurface Handler in der FourCircleInteresectClipPage Klasse verwendet dasselbe SKPath Objekt, um vier überlappende Kreise zu erstellen, von denen jeder den Clippingbereich durch aufeinander folgende Aufrufe ClipPathreduziert:

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);
        }
    }
}

Was links ist die Schnittmenge dieser vier Kreise:

Dreifacher Screenshot der Seite

Die SKClipOperation Enumeration hat nur zwei Elemente:

  • Difference Entfernt den angegebenen Pfad oder Rechteck aus dem vorhandenen Beschneidungsbereich.

  • Intersect Überschneidet den angegebenen Pfad oder Rechteck mit dem vorhandenen Clippingbereich.

Wenn Sie die vier SKClipOperation.Intersect Argumente in der FourCircleIntersectClipPage Klasse durch SKClipOperation.Differenceersetzen, wird Folgendes angezeigt:

Dreifacher Screenshot der Seite

Vier überlappende Kreise wurden aus dem Clippingbereich entfernt.

Auf der Seite "Clip Operations " wird der Unterschied zwischen diesen beiden Vorgängen mit nur einem Kreispaar veranschaulicht. Der erste Kreis auf der linken Seite wird dem Clippingbereich mit dem Standardmäßigen Clipvorgang Intersecthinzugefügt, während der zweite Kreis rechts dem Clippingbereich mit dem durch die Beschriftung angegebenen Clipvorgang hinzugefügt wird:

Dreifacher Screenshot der Seite

Die ClipOperationsPage Klasse definiert zwei SKPaint Objekte als Felder und teilt den Bildschirm dann in zwei rechteckige Bereiche auf. Diese Bereiche unterscheiden sich je nachdem, ob sich das Telefon im Hoch- oder Querformat befindet. Anschließend DisplayClipOp zeigt die Klasse den Text und die Aufrufe ClipPath mit den beiden Kreispfaden an, um die einzelnen Clipvorgänge zu veranschaulichen:

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();
}

Das Aufrufen DrawPaint bewirkt normalerweise, dass der gesamte Zeichenbereich mit diesem SKPaint Objekt gefüllt wird. In diesem Fall zeichnet die Methode jedoch nur innerhalb des Beschneidungsbereichs.

Erkunden von Regionen

Sie können auch einen Beschneidungsbereich in Bezug auf ein SKRegion Objekt definieren.

Ein neu erstelltes SKRegion Objekt beschreibt einen leeren Bereich. Normalerweise ist SetRect der erste Aufruf des Objekts so, dass der Bereich einen rechteckigen Bereich beschreibt. Der Parameter SetRect ist ein SKRectI Wert – ein Rechteck mit ganzzahligen Koordinaten, da er das Rechteck in Bezug auf Pixel angibt. Anschließend können Sie ein SKPath Objekt aufrufenSetPath. Dadurch wird eine Region erstellt, die dem Inneren des Pfads entspricht, aber auf den anfänglichen rechteckigen Bereich zugeschnitten ist.

Der Bereich kann auch geändert werden, indem eine der Op Methodenüberladungen aufgerufen wird, z. B. die folgende:

public Boolean Op(SKRegion region, SKRegionOperation op)

Die SKRegionOperation Enumeration ähnelt SKClipOperation jedoch mehr Elementen:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

Die Region, für die Sie den Op Aufruf ausführen, wird mit der Region kombiniert, die basierend auf dem SKRegionOperation Element als Parameter angegeben ist. Wenn Sie schließlich einen Bereich für den Clipping erhalten, können Sie diesen als Beschneidungsbereich des Zeichenbereichs mit der ClipRegion Methode von SKCanvas:

public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)

Der folgende Screenshot zeigt Clippingbereiche basierend auf den sechs Regionsvorgängen. Der linke Kreis ist der Bereich, für den die Op Methode aufgerufen wird, und der rechte Kreis ist der Bereich, der an die Op Methode übergeben wird:

Dreifacher Screenshot der Seite

Sind dies alle die Möglichkeiten, diese beiden Kreise zu kombinieren? Betrachten Sie das resultierende Bild als Eine Kombination aus drei Komponenten, die selbst in den Difference, Intersectund ReverseDifference Vorgängen zu sehen sind. Die Gesamtanzahl der Kombinationen beträgt zwei bis zur dritten Potenz oder acht. Die beiden fehlenden Regionen sind die ursprüngliche Region (die sich aus keinem Aufruf Op ergibt) und eine völlig leere Region.

Es ist schwieriger, Bereiche für den Clipping zu verwenden, da Sie zuerst einen Pfad und dann einen Bereich aus diesem Pfad erstellen und dann mehrere Regionen kombinieren müssen. Die Gesamtstruktur der Seite "Region-Vorgänge " ist sehr ähnlich wie Clip Operations , die Klasse teilt den Bildschirm jedoch RegionOperationsPage in sechs Bereiche auf und zeigt die zusätzliche Arbeit an, die für die Verwendung von Regionen für diesen Auftrag erforderlich ist:

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();
        }
    }
}

Hier ist ein großer Unterschied zwischen der ClipPath Methode und der ClipRegion Methode:

Wichtig

ClipPath Im Gegensatz zur Methode wird die ClipRegion Methode von Transformationen nicht beeinflusst.

Um die Gründe für diesen Unterschied zu verstehen, ist es hilfreich, zu verstehen, was eine Region ist. Wenn Sie darüber nachgedacht haben, wie die Clipoperationen oder Regionsvorgänge intern implementiert werden könnten, scheint es wahrscheinlich sehr kompliziert. Mehrere potenziell sehr komplexe Pfade werden kombiniert, und die Kontur des resultierenden Pfads ist wahrscheinlich ein algorithmischer Albtraum.

Dieser Auftrag wird erheblich vereinfacht, wenn jeder Weg auf eine Reihe horizontaler Scanlinien reduziert wird, z. B. solche in altmodisch gevakutierten Vakuumröhren-Fernsehgeräten. Jede Scanlinie ist einfach eine horizontale Linie mit einem Startpunkt und einem Endpunkt. Beispielsweise kann ein Kreis mit einem Radius von 10 Pixeln in 20 horizontale Scanlinien zerlegt werden, die jeweils am linken Teil des Kreises beginnen und am rechten Teil enden. Das Kombinieren von zwei Kreisen mit einem beliebigen Bereichsvorgang wird sehr einfach, da es einfach darum geht, die Anfangs- und Endkoordinaten der einzelnen Scanlinienpaare zu untersuchen.

Dies ist eine Region: Eine Reihe von horizontalen Scanlinien, die einen Bereich definieren.

Wenn ein Bereich jedoch auf eine Reihe von Scanzeilen reduziert wird, basieren diese Scanlinien auf einer bestimmten Pixeldimension. Streng genommen ist der Bereich kein Vektorgrafikobjekt. Es ist in der Natur näher an einer komprimierten monochromen Bitmap als an einem Pfad. Folglich können Bereiche nicht skaliert oder gedreht werden, ohne die Genauigkeit zu verlieren, und aus diesem Grund werden sie nicht transformiert, wenn sie für Clippingbereiche verwendet werden.

Sie können jedoch Transformationen für Malzwecke auf Regionen anwenden. Das Programm Region Paint veranschaulicht die innere Natur der Regionen. Die RegionPaintPage Klasse erstellt ein SKRegion Objekt basierend auf einem SKPath Radiuskreis von 10 Einheiten. Eine Transformation erweitert dann diesen Kreis, um die Seite auszufüllen:

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);
            }
        }
    }
}

Der DrawRegion Aufruf füllt den Bereich orange aus, während der DrawPath Ursprüngliche Pfad im Vergleich blau striche:

Dreifacher Screenshot der Seite

Der Bereich ist eindeutig eine Reihe von diskreten Koordinaten.

Wenn Sie Transformationen nicht in Verbindung mit Ihren Beschneidungsbereichen verwenden müssen, können Sie Bereiche zum Ausschneiden verwenden, wie die Seite "Vierblatt-Clover " veranschaulicht. Die FourLeafCloverPage Klasse erstellt einen zusammengesetzten Bereich aus vier kreisförmigen Bereichen, legt diesen zusammengesetzten Bereich als Beschneidungsbereich fest und zeichnet dann eine Reihe von 360 geraden Linien, die von der Mitte der Seite ausgehend werden:

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);
                    }
                }
            }
        }
    }
}

Es sieht nicht wirklich wie ein vierblattiges Kleeblatt aus, aber es ist ein Bild, das andernfalls schwer zu rendern ist, ohne zu schneiden:

Dreifacher Screenshot der Seite