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:
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:
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 SKPath
SKRect
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 ClipPath
reduziert:
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:
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.Difference
ersetzen, wird Folgendes angezeigt:
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 Intersect
hinzugefügt, während der zweite Kreis rechts dem Clippingbereich mit dem durch die Beschriftung angegebenen Clipvorgang hinzugefügt wird:
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:
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
, Intersect
und 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:
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: