Obcinanie przy użyciu ścieżek i regionów
Używanie ścieżek do tworzenia klipów graficznych do określonych obszarów i tworzenia regionów
Czasami konieczne jest ograniczenie renderowania grafiki do określonego obszaru. Jest to nazywane wycinkiem. Można użyć wycinków do efektów specjalnych, takich jak ten obraz małpy widzianej przez dziurkę:
Obszar wycinków to obszar ekranu, w którym renderowana jest grafika. Wszystkie elementy wyświetlane poza obszarem wycinków nie są renderowane. Obszar wycinków jest zwykle definiowany przez prostokąt lub SKPath
obiekt, ale można też zdefiniować obszar wycinków przy użyciu SKRegion
obiektu. Te dwa typy obiektów na początku wydają się powiązane, ponieważ można utworzyć region na podstawie ścieżki. Nie można jednak utworzyć ścieżki z regionu i są one bardzo różne wewnętrznie: ścieżka składa się z serii linii i krzywych, podczas gdy region jest definiowany przez serię linii skanowania poziomego.
Powyższy obraz został utworzony przez małpę za pośrednictwem strony Keyhole . Klasa MonkeyThroughKeyholePage
definiuje ścieżkę przy użyciu danych SVG i używa konstruktora do ładowania mapy bitowej z zasobów programu:
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);
}
}
...
}
keyholePath
Mimo że obiekt opisuje konspekt otworu kluczowego, współrzędne są całkowicie dowolne i odzwierciedlają to, co było wygodne podczas opracowywania danych ścieżki. Z tego powodu PaintSurface
program obsługi uzyskuje granice tej ścieżki i wywołań Translate
oraz Scale
aby przenieść ścieżkę do środka ekranu i ustawić ją niemal tak wysoko, jak na ekranie:
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));
}
}
Ale ścieżka nie jest renderowana. Zamiast tego, po przekształceniach, ścieżka jest używana do ustawiania obszaru wycinków za pomocą tej instrukcji:
canvas.ClipPath(keyholePath);
Następnie PaintSurface
program obsługi resetuje przekształcenia za pomocą wywołania ResetMatrix
i rysuje mapę bitową w celu rozszerzenia do pełnej wysokości ekranu. W tym kodzie przyjęto założenie, że mapa bitowa jest kwadratowa, którą jest ta konkretna mapa bitowa. Mapa bitowa jest renderowana tylko w obrębie obszaru zdefiniowanego przez ścieżkę wycinania:
Ścieżka wycinków podlega przekształceniom, gdy ClipPath
metoda jest wywoływana, a nie do przekształceń w życie, gdy jest wyświetlany obiekt graficzny (taki jak mapa bitowa). Ścieżka wycinkowania jest częścią stanu kanwy zapisanego za Save
pomocą metody i przywróconej za pomocą Restore
metody .
Łączenie ścieżek wycinków
Mówiąc ściśle, obszar wycinków nie jest "ustawiony" przez metodę ClipPath
. Zamiast tego jest ona połączona z istniejącą ścieżką wycinków, która zaczyna się jako prostokąt równy rozmiarowi kanwy. Przy użyciu LocalClipBounds
właściwości lub właściwości można uzyskać prostokątne granice obszaru wycinków DeviceClipBounds
. Właściwość LocalClipBounds
zwraca SKRect
wartość, która odzwierciedla wszelkie przekształcenia, które mogą być w mocy. Właściwość DeviceClipBounds
zwraca RectI
wartość. Jest to prostokąt z wymiarami całkowitymi i opisuje obszar wycinków w rzeczywistych wymiarach pikseli.
Każde wywołanie w celu ClipPath
zmniejszenia obszaru wycinków przez połączenie obszaru wycinków z nowym obszarem. Pełna składnia ClipPath
metody, która łączy obszar wycinków z prostokątem:
public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);
Domyślnie wynikowy obszar wycinków jest przecięciem istniejącego obszaru wycinków i SKPath
wartością lub SKRect
określoną w metodzie ClipPath
or ClipRect
. Jest to pokazane na stronie Przecięcie czterech okręgów. Procedura PaintSurface
obsługi w FourCircleInteresectClipPage
klasie ponownie używa tego samego SKPath
obiektu, aby utworzyć cztery nakładające się okręgi, z których każdy zmniejsza obszar wycinków przez kolejne wywołania metody 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);
}
}
}
Po lewej stronie znajduje się przecięcie tych czterech okręgów:
Wyliczenie SKClipOperation
ma tylko dwa elementy członkowskie:
Difference
usuwa określoną ścieżkę lub prostokąt z istniejącego obszaru wycinkówIntersect
przecina określoną ścieżkę lub prostokąt z istniejącym obszarem wycinków
Jeśli zamienisz cztery SKClipOperation.Intersect
argumenty w FourCircleIntersectClipPage
klasie SKClipOperation.Difference
na , zobaczysz następujące elementy:
Cztery nakładające się okręgi zostały usunięte z obszaru wycinków.
Na stronie Operacje klipu przedstawiono różnicę między tymi dwoma operacjami z parą okręgów. Pierwszy okrąg po lewej stronie jest dodawany do obszaru wycinków z domyślną operacją Intersect
klipu , podczas gdy drugi okrąg po prawej stronie jest dodawany do obszaru wycinków z operacją wycinku wskazywaną przez etykietę tekstową:
Klasa ClipOperationsPage
definiuje dwa SKPaint
obiekty jako pola, a następnie dzieli ekran w górę na dwa prostokątne obszary. Te obszary różnią się w zależności od tego, czy telefon jest w trybie pionowym, czy poziomym. Następnie DisplayClipOp
klasa wyświetla tekst i wywołania ClipPath
z dwoma ścieżkami okręgów, aby zilustrować każdą operację klipu:
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();
}
Wywołanie DrawPaint
zwykle powoduje wypełnienie całej kanwy tym SKPaint
obiektem, ale w tym przypadku metoda po prostu maluje się w obszarze wycinki.
Eksplorowanie regionów
Można również zdefiniować obszar wycinków SKRegion
pod względem obiektu.
Nowo utworzony SKRegion
obiekt opisuje pusty obszar. Zazwyczaj pierwsze wywołanie obiektu polega SetRect
na tym, że region opisuje prostokątny obszar. Parametr to SetRect
SKRectI
wartość — prostokąt ze współrzędnymi całkowitymi, ponieważ określa prostokąt pod względem pikseli. Następnie można wywołać SetPath
obiekt SKPath
. Spowoduje to utworzenie regionu, który jest taki sam jak wnętrze ścieżki, ale przycięty do początkowego prostokątnego regionu.
Region można również zmodyfikować przez wywołanie jednego Op
z przeciążeń metody, takich jak ten:
public Boolean Op(SKRegion region, SKRegionOperation op)
Wyliczenie SKRegionOperation
jest podobne, SKClipOperation
ale ma więcej elementów członkowskich:
Difference
Intersect
Union
XOR
ReverseDifference
Replace
Region, na którym wykonujesz Op
wywołanie, jest połączony z regionem określonym jako parametr na podstawie elementu SKRegionOperation
członkowskiego. Gdy na koniec uzyskasz region odpowiedni do przycinania, możesz ustawić go jako obszar wycinków kanwy przy użyciu ClipRegion
metody SKCanvas
:
public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)
Poniższy zrzut ekranu przedstawia wycinki obszarów na podstawie sześciu operacji w regionie. Lewy okrąg to region wywoływany przez Op
metodę, a prawy okrąg jest regionem przekazanym Op
do metody:
Czy są to wszystkie możliwości łączenia tych dwóch okręgów? Rozważ wynikowy obraz jako kombinację trzech składników, które same w sobie są widoczne w Difference
operacjach , Intersect
i ReverseDifference
. Łączna liczba kombinacji wynosi od dwóch do trzeciej mocy lub ośmiu. Te dwa, których brakuje, to oryginalny region (który wynika z braku wywołania Op
w ogóle) i całkowicie pusty region.
Trudniej jest używać regionów do tworzenia wycinków, ponieważ musisz najpierw utworzyć ścieżkę, a następnie region z tej ścieżki, a następnie połączyć wiele regionów. Ogólna struktura strony Operacje regionów jest bardzo podobna do operacji clip, ale RegionOperationsPage
klasa dzieli ekran w górę na sześć obszarów i pokazuje dodatkową pracę wymaganą do korzystania z regionów dla tego zadania:
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();
}
}
}
Oto duża różnica między ClipPath
metodą a ClipRegion
metodą:
Ważne
ClipPath
W przeciwieństwie do metody ClipRegion
metoda ta nie ma wpływu na przekształcenia.
Aby zrozumieć uzasadnienie tej różnicy, warto zrozumieć, czym jest region. Jeśli zastanawiasz się, jak operacje klipów lub operacje regionów mogą być implementowane wewnętrznie, prawdopodobnie wydaje się to bardzo skomplikowane. Łączy się kilka potencjalnie bardzo złożonych ścieżek, a konspekt wynikowej ścieżki jest prawdopodobnie algorytmicznym koszmarem.
To zadanie jest znacznie uproszczone, jeśli każda ścieżka jest ograniczona do serii linii skanowania poziomego, takich jak te w staroświetlonych telewizorach próżniowych. Każda linia skanowania to po prostu linia pozioma z punktem początkowym i punktem końcowym. Na przykład okrąg z promieniem 10 pikseli można rozkładać na 20 linii skanowania poziomego, z których każdy zaczyna się od lewej części okręgu i kończy się po prawej stronie. Połączenie dwóch okręgów z dowolną operacją regionu staje się bardzo proste, ponieważ jest to po prostu kwestia badania współrzędnych początkowych i końcowych każdej pary odpowiednich linii skanowania.
To jest region: Seria linii skanowania poziomego, które definiują obszar.
Jednak po zmniejszeniu obszaru do serii linii skanowania te linie skanowania są oparte na określonym wymiarze pikseli. Mówiąc ściśle, region nie jest obiektem grafiki wektorowej. Jest bliżej natury skompresowanej mapy bitowej monochromatycznej niż do ścieżki. W związku z tym regiony nie mogą być skalowane ani obracane bez utraty wierności, a z tego powodu nie są przekształcane w przypadku używania do wycinków obszarów.
Można jednak zastosować przekształcenia do regionów do celów malowania. Program Malowanie regionów wyraźnie pokazuje wewnętrzny charakter regionów. Klasa RegionPaintPage
tworzy SKRegion
obiekt na SKPath
podstawie okręgu promień 10 jednostek. Następnie przekształcenie rozszerza ten okrąg, aby wypełnić stronę:
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);
}
}
}
}
Wywołanie DrawRegion
wypełnia region kolorem pomarańczowym, podczas gdy DrawPath
wywołanie uderza oryginalną ścieżkę w kolorze niebieskim w celu porównania:
Region jest wyraźnie serią dyskretnych współrzędnych.
Jeśli nie musisz używać przekształceń w połączeniu z obszarami wycinków, możesz użyć regionów do wycinków, jak pokazuje strona Four-Leaf Clover . Klasa FourLeafCloverPage
tworzy region złożony z czterech regionów okrągłych, ustawia ten region złożony jako obszar wycinki, a następnie rysuje serię 360 linii prostych emanujących od środka strony:
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);
}
}
}
}
}
}
To naprawdę nie wygląda jak czterolistna koniczyna, ale jest to obraz, który w przeciwnym razie może być trudny do renderowania bez wycinków: