Ritaglio con tracciati e aree

Download Sample Scaricare l'esempio

Usare i percorsi per ritagliare elementi grafici in aree specifiche e per creare aree

A volte è necessario limitare il rendering della grafica a una determinata area. Questa operazione è nota come ritaglio. Puoi usare il ritaglio per effetti speciali, ad esempio questa immagine di una scimmia vista attraverso un buco delle chiavi:

Monkey through a keyhole

L'area di ritaglio è l'area dello schermo in cui viene eseguito il rendering della grafica. Non viene eseguito il rendering di tutti gli elementi visualizzati all'esterno dell'area di ritaglio. L'area di ritaglio è in genere definita da un rettangolo o da un SKPath oggetto, ma in alternativa è possibile definire un'area di ritaglio usando un SKRegion oggetto . Questi due tipi di oggetti sembrano inizialmente correlati perché è possibile creare un'area da un percorso. Tuttavia, non è possibile creare un percorso da un'area e sono molto diversi internamente: un percorso comprende una serie di linee e curve, mentre un'area è definita da una serie di linee di analisi orizzontali.

L'immagine precedente è stata creata dalla pagina Monkey tramite Keyhole . La MonkeyThroughKeyholePage classe definisce un percorso usando i dati SVG e usa il costruttore per caricare una bitmap dalle risorse del programma:

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

Sebbene l'oggetto keyholePath descriva la struttura di un foro delle chiavi, le coordinate sono completamente arbitrarie e riflettono ciò che è stato conveniente quando sono stati elaborati i dati del percorso. Per questo motivo, il PaintSurface gestore ottiene i limiti di questo percorso e chiama Translate e Scale per spostare il percorso al centro dello schermo e per renderlo quasi più alto dello schermo:

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

Ma il rendering del percorso non viene eseguito. Al contrario, seguendo le trasformazioni, il percorso viene usato per impostare un'area di ritaglio con questa istruzione:

canvas.ClipPath(keyholePath);

Il PaintSurface gestore reimposta quindi le trasformazioni con una chiamata a ResetMatrix e disegna la bitmap per estenderla all'altezza intera dello schermo. Questo codice presuppone che la bitmap sia quadrata, ovvero questa particolare bitmap. Il rendering della bitmap viene eseguito solo all'interno dell'area definita dal percorso di ritaglio:

Triple screenshot of the Monkey through Keyhole page

Il percorso di ritaglio è soggetto alle trasformazioni in vigore quando viene chiamato il ClipPath metodo e non alle trasformazioni in vigore quando viene visualizzato un oggetto grafico (ad esempio una bitmap). Il percorso di ritaglio fa parte dello stato dell'area di disegno salvato con il Save metodo e ripristinato con il Restore metodo .

Combinazione di percorsi di ritaglio

In senso stretto, l'area di ritaglio non è "impostata" dal ClipPath metodo . Viene invece combinato con il percorso di ritaglio esistente, che inizia come rettangolo di dimensioni uguali all'area di disegno. È possibile ottenere i limiti rettangolari dell'area di ritaglio utilizzando la LocalClipBounds proprietà o la DeviceClipBounds proprietà . La LocalClipBounds proprietà restituisce un SKRect valore che riflette tutte le trasformazioni che potrebbero essere effettive. La DeviceClipBounds proprietà restituisce un RectI valore. Si tratta di un rettangolo con dimensioni integer e descrive l'area di ritaglio in dimensioni pixel effettive.

Qualsiasi chiamata per ClipPath ridurre l'area di ritaglio combinando l'area di ritaglio con una nuova area. Sintassi completa del ClipPath metodo che combina l'area di ritaglio con un rettangolo:

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

Per impostazione predefinita, l'area di ritaglio risultante è un'intersezione dell'area di ritaglio esistente e dell'oggetto SKPathClipPath o SKRect specificato nel metodo o ClipRect . Questo è dimostrato nella pagina Quattro cerchi interseca clip . Il PaintSurface gestore nella FourCircleInteresectClipPage classe riutilizza lo stesso SKPath oggetto per creare quattro cerchi sovrapposti, ognuno dei quali riduce l'area di ritaglio tramite chiamate successive a 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);
        }
    }
}

A sinistra c'è l'intersezione di questi quattro cerchi:

Triple screenshot of the Four Circle Intersect Clip page

L'enumerazione SKClipOperation ha solo due membri:

  • Difference rimuove il percorso o il rettangolo specificato dall'area di ritaglio esistente

  • Intersect interseca il percorso o il rettangolo specificato con l'area di ritaglio esistente

Se si sostituiscono i quattro SKClipOperation.Intersect argomenti nella FourCircleIntersectClipPage classe con SKClipOperation.Difference, verrà visualizzato quanto segue:

Triple screenshot of the Four Circle Intersect Clip page with difference operation

Quattro cerchi sovrapposti sono stati rimossi dall'area di ritaglio.

La pagina Operazioni ritaglia illustra la differenza tra queste due operazioni con solo una coppia di cerchi. Il primo cerchio a sinistra viene aggiunto all'area di ritaglio con l'operazione di ritaglio predefinita di Intersect, mentre il secondo cerchio a destra viene aggiunto all'area di ritaglio con l'operazione di ritaglio indicata dall'etichetta di testo:

Triple screenshot of the Clip Operations page

La ClipOperationsPage classe definisce due SKPaint oggetti come campi e quindi divide lo schermo in due aree rettangolari. Queste aree sono diverse a seconda che il telefono sia in modalità verticale o orizzontale. La DisplayClipOp classe visualizza quindi il testo e le chiamate ClipPath con i due percorsi circolari per illustrare ogni operazione di ritaglio:

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

La chiamata DrawPaint in genere fa sì che l'intera area di disegno venga riempita con tale SKPaint oggetto, ma in questo caso il metodo disegna solo all'interno dell'area di ritaglio.

Esplorazione delle aree

È anche possibile definire un'area di ritaglio in termini di oggetto SKRegion .

Un oggetto appena creato SKRegion descrive un'area vuota. In genere la prima chiamata sull'oggetto è SetRect in modo che l'area descriva un'area rettangolare. Il parametro a SetRect è un SKRectI valore, ovvero un rettangolo con coordinate integer perché specifica il rettangolo in termini di pixel. È quindi possibile chiamare SetPath con un SKPath oggetto . In questo modo viene creata un'area uguale all'interno del percorso, ma ritagliata nell'area rettangolare iniziale.

L'area può anche essere modificata chiamando uno degli overload del Op metodo, ad esempio questo:

public Boolean Op(SKRegion region, SKRegionOperation op)

L'enumerazione SKRegionOperation è simile a SKClipOperation ma ha più membri:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

L'area su cui si effettua la Op chiamata viene combinata con l'area specificata come parametro in base al SKRegionOperation membro. Quando si ottiene infine un'area adatta per il ritaglio, è possibile impostarlo come area di ritaglio dell'area di disegno usando il ClipRegion metodo di SKCanvas:

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

Lo screenshot seguente mostra le aree di ritaglio in base alle sei operazioni dell'area. Il cerchio sinistro è l'area in cui viene chiamato il Op metodo e il cerchio destro è l'area passata al Op metodo :

Triple screenshot of the Region Operations page

Queste sono tutte le possibilità di combinare questi due cerchi? Si consideri l'immagine risultante come una combinazione di tre componenti, che da soli vengono visualizzati nelle Differenceoperazioni , Intersecte ReverseDifference . Il numero totale di combinazioni è due alla terza potenza o otto. I due elementi mancanti sono l'area originale (che risulta da non chiamare Op affatto) e un'area completamente vuota.

È più difficile usare le aree per il ritaglio perché è prima necessario creare un percorso e quindi un'area da tale percorso e quindi combinare più aree. La struttura complessiva della pagina Operazioni area è molto simile a Operazioni clip, ma la RegionOperationsPage classe divide lo schermo in sei aree e mostra il lavoro aggiuntivo necessario per usare le aree per questo processo:

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

Ecco una grande differenza tra il ClipPath metodo e il ClipRegion metodo :

Importante

A differenza del ClipPath metodo , il ClipRegion metodo non è interessato dalle trasformazioni.

Per comprendere la logica di questa differenza, è utile comprendere che cos'è un'area. Se si è pensato a come le operazioni di ritaglio o di area potrebbero essere implementate internamente, probabilmente sembra molto complicato. Vengono combinati diversi percorsi potenzialmente molto complessi e la struttura del percorso risultante è probabilmente un incubo algoritmico.

Questo lavoro è notevolmente semplificato se ogni percorso è ridotto a una serie di linee di scansione orizzontale, come quelle nei televisori a tubo a vuoto vecchio stile. Ogni linea di analisi è semplicemente una linea orizzontale con un punto iniziale e un punto finale. Ad esempio, un cerchio con un raggio di 10 pixel può essere scomposto in 20 linee di scansione orizzontale, ognuna delle quali inizia nella parte sinistra del cerchio e termina nella parte destra. La combinazione di due cerchi con qualsiasi operazione di area diventa molto semplice perché è semplicemente una questione di esaminare le coordinate iniziale e finale di ogni coppia di linee di analisi corrispondenti.

Si tratta di un'area: una serie di linee di analisi orizzontale che definiscono un'area.

Tuttavia, quando un'area viene ridotta a una serie di linee di analisi, queste linee di analisi si basano su una determinata dimensione pixel. In senso stretto, l'area non è un oggetto grafico vettoriale. La natura è più vicina a una bitmap monocromatica compressa rispetto a un percorso. Di conseguenza, le aree non possono essere ridimensionate o ruotate senza perdere fedeltà e per questo motivo non vengono trasformate quando vengono usate per ritagliare le aree.

Tuttavia, è possibile applicare trasformazioni alle aree a scopo di disegno. Il programma Region Paint illustra in modo vivido la natura interna delle regioni. La RegionPaintPage classe crea un SKRegion oggetto basato su un SKPath cerchio raggio di 10 unità. Una trasformazione espande quindi il cerchio per riempire la pagina:

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

La DrawRegion chiamata riempie l'area in arancione, mentre la DrawPath chiamata traccia il percorso originale in blu per il confronto:

Triple screenshot of the Region Paint page

L'area è chiaramente una serie di coordinate discrete.

Se non è necessario usare trasformazioni in relazione alle aree di ritaglio, è possibile usare le aree per il ritaglio, come illustrato nella pagina Four-Leaf Clover . La FourLeafCloverPage classe costruisce un'area composita da quattro aree circolari, imposta l'area composita come area di ritaglio e quindi disegna una serie di 360 linee rette che provengono dal centro della pagina:

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

Non ha un aspetto davvero simile a un trifoglio a quattro foglie, ma è un'immagine che potrebbe altrimenti essere difficile da eseguire senza ritagliare:

Triple screenshot of the Four-Leaf Clover page