Udostępnij za pośrednictwem


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

Małpa przez dziurę kluczową

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:

Potrójny zrzut ekranu przedstawiający stronę Monkey through Keyhole

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

Potrójny zrzut ekranu przedstawiający stronę Wycinki z czterema okręgami

Wyliczenie SKClipOperation ma tylko dwa elementy członkowskie:

  • Difference usuwa określoną ścieżkę lub prostokąt z istniejącego obszaru wycinków

  • Intersect 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.Differencena , zobaczysz następujące elementy:

Potrójny zrzut ekranu przedstawiający stronę Wycinki z czterema okręgami z operacją różnicy

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ą Intersectklipu , podczas gdy drugi okrąg po prawej stronie jest dodawany do obszaru wycinków z operacją wycinku wskazywaną przez etykietę tekstową:

Potrójny zrzut ekranu przedstawiający stronę Operacje klipu

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

Zrzut ekranu przedstawiający stronę Operacje regionów

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 Differenceoperacjach , Intersecti 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:

Potrójny zrzut ekranu przedstawiający stronę Malowanie regionów

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:

Potrójny zrzut ekranu przedstawiający stronę Clover z czterema liśćmi