Compartilhar via


Recorte com caminhos de regiões

Usar caminhos para recortar gráficos para áreas específicas e para criar regiões

Às vezes, é necessário restringir a renderização de gráficos a uma área específica. Isso é conhecido como clipping. Você pode usar o recorte para efeitos especiais, como esta imagem de um macaco visto através de um buraco de fechadura:

Macaco através de um buraco de fechadura

A área de recorte é a área da tela na qual os gráficos são renderizados. Tudo o que é exibido fora da área de recorte não é renderizado. A área de recorte geralmente é definida por um retângulo ou um SKPath objeto, mas você pode alternativamente definir uma área de recorte usando um SKRegion objeto. Esses dois tipos de objetos a princípio parecem relacionados porque você pode criar uma região a partir de um caminho. No entanto, você não pode criar um caminho a partir de uma região, e eles são muito diferentes internamente: um caminho compreende uma série de linhas e curvas, enquanto uma região é definida por uma série de linhas de varredura horizontais.

A imagem acima foi criada pela página Macaco através do Keyhole . A MonkeyThroughKeyholePage classe define um caminho usando dados SVG e usa o construtor para carregar um bitmap de recursos do programa:

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

Embora o keyholePath objeto descreva o contorno de um buraco de fechadura, as coordenadas são completamente arbitrárias e refletem o que era conveniente quando os dados do caminho foram criados. Por esse motivo, o PaintSurface manipulador obtém os limites desse caminho e chama Translate e Scale move o caminho para o centro da tela e o torna quase tão alto quanto a tela:

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

Mas o caminho não é traçado. Em vez disso, após as transformações, o caminho é usado para definir uma área de recorte com esta instrução:

canvas.ClipPath(keyholePath);

Em PaintSurface seguida, o manipulador redefine as transformações com uma chamada para ResetMatrix e desenha o bitmap para estender até a altura total da tela. Esse código pressupõe que o bitmap é quadrado, o que esse bitmap específico é. O bitmap é renderizado somente dentro da área definida pelo caminho de recorte:

Captura de tela tripla da página Macaco através do Keyhole

O caminho de recorte está sujeito às transformações em vigor quando o ClipPath método é chamado, e não às transformações em vigor quando um objeto gráfico (como um bitmap) é exibido. O caminho de recorte faz parte do estado da tela que é salvo com o Save método e restaurado com o Restore método.

Combinando caminhos de recorte

A rigor, a área de recorte não é "definida" pelo ClipPath método. Em vez disso, ele é combinado com o caminho de recorte existente, que começa como um retângulo igual em tamanho à tela. Você pode obter os limites retangulares da área de recorte usando a LocalClipBounds propriedade ou a DeviceClipBounds propriedade. A LocalClipBounds propriedade retorna um SKRect valor que reflete quaisquer transformações que possam estar em vigor. A DeviceClipBounds propriedade retorna um RectI valor. Este é um retângulo com dimensões inteiras e descreve a área de recorte em dimensões reais de pixel.

Qualquer chamada para ClipPath reduz a área de recorte combinando a área de recorte com uma nova área. A sintaxe completa do ClipPath método que combina a área de recorte com um retângulo:

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

Por padrão, a área de recorte resultante é uma interseção da área de recorte existente e o SKPath ou SKRect especificado no ClipPath método or ClipRect . Isso é demonstrado na página Four Circles Intersect Clip . O PaintSurface manipulador na FourCircleInteresectClipPage classe reutiliza o mesmo SKPath objeto para criar quatro círculos sobrepostos, cada um dos quais reduz a área de recorte por meio de chamadas sucessivas para 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);
        }
    }
}

O que resta é a intersecção desses quatro círculos:

Captura de tela tripla da página Clipe de interseção de quatro círculos

A SKClipOperation enumeração tem apenas dois membros:

  • Difference Remove o caminho ou retângulo especificado da área de recorte existente

  • Intersect cruza o caminho ou retângulo especificado com a área de recorte existente

Se você substituir os quatro SKClipOperation.Intersect argumentos na FourCircleIntersectClipPage classe por SKClipOperation.Difference, verá o seguinte:

Captura de tela tripla da página Clipe de interseção de quatro círculos com operação de diferença

Quatro círculos sobrepostos foram removidos da área de recorte.

A página Operações de Clipe ilustra a diferença entre essas duas operações com apenas um par de círculos. O primeiro círculo à esquerda é adicionado à área de recorte com a operação de clipe padrão de , enquanto o segundo círculo à direita é adicionado à área de recorte com a operação de clipe indicada pelo rótulo de Intersecttexto:

Captura de tela tripla da página Operações de Clipe

A ClipOperationsPage classe define dois SKPaint objetos como campos e, em seguida, divide a tela em duas áreas retangulares. Essas áreas são diferentes dependendo se o telefone está no modo retrato ou paisagem. Em DisplayClipOp seguida, a classe exibe o texto e as chamadas ClipPath com os dois caminhos de círculo para ilustrar cada operação de clipe:

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

A chamada DrawPaint normalmente faz com que a tela inteira seja preenchida com esse SKPaint objeto, mas, nesse caso, o método apenas pinta dentro da área de recorte.

Explorando Regiões

Você também pode definir uma área de recorte em termos de um SKRegion objeto.

Um objeto recém-criado SKRegion descreve uma área vazia. Normalmente, a primeira chamada no objeto é SetRect para que a região descreva uma área retangular. O parâmetro to SetRect é um SKRectI valor — um retângulo com coordenadas inteiras porque especifica o retângulo em termos de pixels. Em seguida, você pode chamar SetPath com um SKPath objeto. Isso cria uma região que é a mesma do interior do caminho, mas recortada para a região retangular inicial.

A região também pode ser modificada chamando uma das sobrecargas de Op método, como esta:

public Boolean Op(SKRegion region, SKRegionOperation op)

A SKRegionOperation enumeração é semelhante SKClipOperation , mas tem mais membros:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

A região na qual você está fazendo a Op chamada é combinada com a região especificada como um parâmetro com base no SKRegionOperation membro. Quando você finalmente obtém uma região adequada para recorte, você pode defini-la como a área de recorte da tela usando o ClipRegion método de SKCanvas:

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

A captura de tela a seguir mostra áreas de recorte com base nas operações de seis regiões. O círculo esquerdo é a região em que o Op método é chamado e o círculo direito é a região passada para o Op método:

Captura de tela tripla da página Operações de Região

São todas essas as possibilidades de combinar esses dois círculos? Considere a imagem resultante como uma combinação de três componentes, que por si só são vistos nas Differenceoperações , Intersecte ReverseDifference . O número total de combinações é de dois para a terceira potência, ou oito. As duas que faltam são a região original (que resulta de não chamar Op nada) e uma região totalmente vazia.

É mais difícil usar regiões para recorte porque você precisa primeiro criar um caminho e, em seguida, uma região desse caminho e, em seguida, combinar várias regiões. A estrutura geral da página Operações de Região é muito semelhante às Operações de Clipe, mas a RegionOperationsPage classe divide a tela em seis áreas e mostra o trabalho extra necessário para usar regiões para esse trabalho:

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

Aqui está uma grande diferença entre o ClipPath método e o ClipRegion método:

Importante

Ao contrário do ClipPath método, o ClipRegion método não é afetado por transformações.

Para entender a razão dessa diferença, é útil entender o que é uma região. Se você pensou em como as operações de clipe ou as operações de região podem ser implementadas internamente, provavelmente parece muito complicado. Vários caminhos potencialmente muito complexos estão sendo combinados, e o esboço do caminho resultante é provavelmente um pesadelo algorítmico.

Esse trabalho é consideravelmente simplificado se cada caminho for reduzido a uma série de linhas de varredura horizontais, como as das antigas TVs de tubo de vácuo. Cada linha de varredura é simplesmente uma linha horizontal com um ponto inicial e um ponto final. Por exemplo, um círculo com um raio de 10 pixels pode ser decomposto em 20 linhas de varredura horizontais, cada uma das quais começa na parte esquerda do círculo e termina na parte direita. Combinar dois círculos com qualquer operação de região torna-se muito simples porque é simplesmente uma questão de examinar as coordenadas de início e fim de cada par de linhas de varredura correspondentes.

Isto é o que é uma região: uma série de linhas de varredura horizontais que definem uma área.

No entanto, quando uma área é reduzida a uma série de linhas de varredura, essas linhas de varredura são baseadas em uma dimensão de pixel específica. Estritamente falando, a região não é um objeto gráfico vetorial. Ele está mais próximo na natureza de um bitmap monocromático compactado do que de um caminho. Consequentemente, as regiões não podem ser dimensionadas ou giradas sem perder a fidelidade e, por esse motivo, não são transformadas quando usadas para áreas de recorte.

No entanto, você pode aplicar transformações a regiões para fins de pintura. O programa Region Paint demonstra vividamente a natureza interna das regiões. A RegionPaintPage classe cria um SKRegion objeto com base em um SKPath círculo de raio de 10 unidades. Em seguida, uma transformação expande esse círculo para preencher a página:

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

A DrawRegion chamada preenche a região em laranja, enquanto a DrawPath chamada traça o caminho original em azul para comparação:

Captura de tela tripla da página Pintura de Região

A região é claramente uma série de coordenadas discretas.

Se você não precisar usar transformações em conexão com suas áreas de recorte, poderá usar regiões para recorte, como demonstra a página Trevo de Quatro Folhas. A FourLeafCloverPage classe constrói uma região composta a partir de quatro regiões circulares, define essa região composta como a área de recorte e, em seguida, desenha uma série de 360 linhas retas emanando do centro da página:

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

Ele realmente não se parece com um trevo de quatro folhas, mas é uma imagem que poderia ser difícil de renderizar sem recorte:

Captura de tela tripla da página Trevo de Quatro Folhas