Sdílet prostřednictvím


Ořezy cestami a oblastmi

Použití cest k oříznutí grafiky do konkrétních oblastí a vytváření oblastí

Někdy je nutné omezit vykreslování grafiky na konkrétní oblast. To se označuje jako výřez. Výřez můžete použít pro speciální efekty, jako je například tento obrázek opice, kterou vidíte prostřednictvím klíčového otvoru:

Opice přes keyhole

Oblast výřezu je oblast obrazovky, ve které se vykreslují grafické objekty. Všechno, co se zobrazí mimo oblast výřezu, se nevykreslí. Oblast výřezu je obvykle definována obdélníkem SKPath nebo objektem, ale můžete alternativně definovat oblast výřezu pomocí objektu SKRegion . Tyto dva typy objektů se zpočátku zdají být související, protože můžete vytvořit oblast z cesty. Nemůžete však vytvořit cestu z oblasti a jsou velmi odlišné interně: Cesta se skládá z řady čar a křivek, zatímco oblast je definována řadou vodorovných čar skenování.

Výše uvedený obrázek vytvořila stránka Monkey through Keyhole . Třída MonkeyThroughKeyholePage definuje cestu pomocí dat SVG a pomocí konstruktoru načte rastrový obrázek z programových prostředků:

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

I když objekt keyholePath popisuje obrys klíčového tlačítka, souřadnice jsou zcela libovolné a odrážejí, co bylo vhodné, když byla data cesty navržena. Z tohoto důvodu PaintSurface obslužná rutina získá hranice této cesty a volání Translate a Scale přesune cestu do středu obrazovky a aby byla téměř tak vysoká jako obrazovka:

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

Cesta se ale nevykreslí. Místo toho se cesta použije k nastavení oblasti výřezu pomocí tohoto příkazu:

canvas.ClipPath(keyholePath);

Obslužná rutina PaintSurface pak transformuje voláním ResetMatrix a nakreslí rastrový obrázek tak, aby se rozšířil na celou výšku obrazovky. Tento kód předpokládá, že rastrový obrázek je čtvercový, což je tento konkrétní rastrový obrázek. Rastrový obrázek se vykreslí pouze v oblasti definované cestou výřezu:

Triple screenshot of the Monkey through Keyhole page

Cesta výřezu podléhá transformacím, pokud ClipPath je volána metoda, a nikoli transformací, pokud se zobrazí grafický objekt (například rastrový obrázek). Cesta výřezu je součástí stavu plátna, který je uložen s metodou Save a obnoven pomocí Restore metody.

Kombinování cest výřezů

Přesněji řečeno, oblast výřezu není "nastavena" metodou ClipPath . Místo toho se zkombinuje s existující cestou výřezu, která začíná jako obdélník se stejnou velikostí jako plátno. Obdélníkové hranice oblasti výřezu můžete získat pomocí LocalClipBounds vlastnosti nebo DeviceClipBounds vlastnosti. Vlastnost LocalClipBounds vrátí SKRect hodnotu, která odráží všechny transformace, které mohou být účinné. Vlastnost DeviceClipBounds vrátí RectI hodnotu. Toto je obdélník s celočíselnou dimenzí a popisuje oblast výřezu ve skutečných rozměrech pixelů.

Jakékoli volání ClipPath pro zmenšení oblasti výřezu zkombinováním oblasti výřezu s novou oblastí. Úplná syntaxe ClipPath metody, která kombinuje oblast výřezu s obdélníkem:

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

Výsledná oblast výřezu je ve výchozím nastavení průsečíkem existující oblasti výřezu SKPath a oblastí, SKRect která je zadána ClipPath v metodě nebo ClipRect metodě. To je znázorněno na stránce Čtyř kruhů protínající klip . Obslužná rutina PaintSurface ve FourCircleInteresectClipPage třídě znovu použije stejný SKPath objekt k vytvoření čtyř překrývajících se kruhů, z nichž každá z nich zmenšuje oblast výřezu prostřednictvím následných volání 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);
        }
    }
}

Vlevo je průsečík těchto čtyř kruhů:

Trojitý snímek obrazovky se stránkou Klip protínající čtyři kruhy

Výčet SKClipOperation má pouze dva členy:

  • Difference odebere zadanou cestu nebo obdélník z existující oblasti výřezu.

  • Intersect protíná zadanou cestu nebo obdélník s existující oblastí výřezu.

Pokud nahradíte čtyři SKClipOperation.Intersect argumenty ve FourCircleIntersectClipPage třídě SKClipOperation.Difference, zobrazí se následující:

Trojitý snímek obrazovky se stránkou Klip protínající čtyři kruhy s operací rozdílu

Z oblasti výřezu byly odebrány čtyři překrývající se kruhy.

Stránka Operace klipů znázorňuje rozdíl mezi těmito dvěma operacemi jenom párem kruhů. Do oblasti výřezu se přidá první kruh vlevo s výchozí operací klipartu Intersect, zatímco druhý kruh vpravo se přidá do oblasti výřezu pomocí operace klipartu označené textovým popiskem:

Trojitý snímek obrazovky se stránkou Operace klipů

Třída ClipOperationsPage definuje dva SKPaint objekty jako pole a pak rozdělí obrazovku nahoru do dvou obdélníkových oblastí. Tyto oblasti se liší v závislosti na tom, jestli je telefon v režimu na výšku nebo na šířku. Třída DisplayClipOp pak zobrazí text a volání ClipPath se dvěma kruhovými cestami k ilustraci každé operace 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();
}

Volání DrawPaint obvykle způsobí, že celé plátno bude vyplněno tímto SKPaint objektem, ale v tomto případě metoda pouze maluje v oblasti výřezu.

Prozkoumání oblastí

Oblast výřezu můžete také definovat z hlediska objektu SKRegion .

Nově vytvořený SKRegion objekt popisuje prázdnou oblast. Prvním voláním objektu je SetRect obvykle to, že oblast popisuje obdélníkovou oblast. Parametr SetRect je SKRectI hodnota – obdélník se souřadnicemi celého čísla, protože určuje obdélník z hlediska pixelů. Potom můžete volat SetPath s objektem SKPath . Tím se vytvoří oblast, která je stejná jako vnitřní část cesty, ale oříznutá na počáteční obdélníkovou oblast.

Oblast lze také upravit voláním jednoho z Op přetížení metody, například tímto:

public Boolean Op(SKRegion region, SKRegionOperation op)

Výčet SKRegionOperation se podobá SKClipOperation výčtu, ale má více členů:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

Oblast, na které voláte Op , se zkombinuje s oblastí zadanou jako parametr na základě člena SKRegionOperation . Když nakonec získáte oblast vhodná pro výřez, můžete ji nastavit jako oblast výřezu plátna pomocí ClipRegion metody SKCanvas:

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

Následující snímek obrazovky ukazuje výřez oblastí založených na šesti operacích oblasti. Levý kruh je oblast, na Op které je metoda volána, a pravý kruh je oblast předaná Op metodě:

Trojitý snímek obrazovky se stránkou Operace v oblasti

Jsou tyto všechny možnosti kombinování těchto dvou kruhů? Představte si výslednou image jako kombinaci tří komponent, které jsou samy o sobě vidět v Difference, Intersecta ReverseDifference operacích. Celkový početkombinacích Chybějící dvě oblasti jsou původní oblastí (což má za následek nevolání Op vůbec) a úplně prázdnou oblast.

Při výřezu je obtížnější použít oblasti, protože je potřeba nejprve vytvořit cestu a pak oblast z této cesty a pak zkombinovat více oblastí. Celková struktura stránky Operace v oblasti je velmi podobná operacím klipů , ale RegionOperationsPage třída rozdělí obrazovku nahoru do šesti oblastí a zobrazuje nadbytečnou práci potřebnou k použití oblastí pro tuto úlohu:

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

Tady je velký rozdíl mezi metodou ClipPath a metodou ClipRegion :

Důležité

ClipPath Na rozdíl od metody ClipRegion není metoda ovlivněna transformací.

Abyste pochopili odůvodnění tohoto rozdílu, je užitečné pochopit, co je oblast. Pokud jste přemýšleli o tom, jak se operace klipů nebo oblasti můžou implementovat interně, pravděpodobně se zdá být velmi složité. Zkombinuje se několik potenciálně složitých cest a osnova výsledné cesty je pravděpodobně algoritmickou noční můrou.

Tato úloha je značně zjednodušená, pokud je každá cesta omezena na řadu vodorovných skenových čar, jako jsou ty ve starých vakuových trubicích. Každá čára skenování je jednoduše vodorovná čára s počátečním bodem a koncovým bodem. Například kruh s poloměrem 10 pixelů lze rozdělit do 20 vodorovných čar skenování, z nichž každá začíná v levé části kruhu a končí v pravé části. Kombinace dvou kruhů s libovolnou operací oblasti se stává velmi jednoduchou, protože je to jednoduše otázka zkoumání počátečních a koncových souřadnic každé dvojice odpovídajících čar skenování.

To je oblast: Řada vodorovných čar skenování, které definují oblast.

Pokud se ale oblast zmenší na řadu skenovaných čar, jsou tyto čáry skenování založeny na konkrétní dimenzi pixelu. Přesněji řečeno, oblast není vektorový grafický objekt. Je v přírodě blíže komprimovanému monochromatickému rastru než k cestě. V důsledku toho nelze oblasti škálovat ani otáčet bez ztráty věrnosti, a proto se při použití pro výřezy neproměňují.

Transformace však můžete použít pro oblasti pro účely obrazu. Oblast Malování program živě ukazuje vnitřní povahu oblastí. Třída RegionPaintPage vytvoří SKRegion objekt založený na kruhu SKPath poloměru 10 jednotek. Transformace pak tento kruh rozbalí, aby vyplnil stránku:

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

Volání DrawRegion vyplní oblast oranžovou barvou, zatímco DrawPath volání pro porovnání tahá původní cestu modře:

Trojitý snímek obrazovky se stránkou Oblast Malování

Oblast je jasně řadou samostatných souřadnic.

Pokud v souvislosti s oblastmi výřezu nepotřebujete používat transformace, můžete k výřezu použít oblasti, jak ukazuje stránka Four-Leaf Clover . Třída FourLeafCloverPage vytvoří složenou oblast ze čtyř kruhových oblastí, nastaví danou složenou oblast jako oblast výřezu a pak nakreslí řadu 360 rovných čar pocházejících ze středu stránky:

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

Ve skutečnosti nevypadá jako čtyřlistý clover, ale je to obrázek, který by jinak mohl být těžké vykreslit bez výřezu:

Triple screenshot of the Four-Leaf Clover page