Recorte con trazados y regiones

Download SampleDescargar el ejemplo

Uso de trazados para recortar gráficos a áreas específicas y para crear regiones

A veces es necesario restringir la representación de gráficos a un área determinada. Esto se conoce como recorte. Puede usar recortes para crear efectos especiales, como esta imagen de un mono visto a través de una cerradura:

Monkey through a keyhole

El área de recorte es el área de la pantalla en la que se representan los gráficos. No se representa nada que se muestre fuera del área de recorte. Normalmente, el área de recorte se define mediante un rectángulo o un objeto SKPath, pero también se puede definir mediante un objeto SKRegion. Al principio estos dos tipos de objetos parecen relacionados porque se puede crear una región a partir de un trazado. Sin embargo, no se puede crear un trazado a partir de una región, y ambos son muy diferentes internamente: un trazado consta de una serie de líneas y curvas, mientras que una región se define mediante una serie de líneas de barrido horizontales.

La imagen anterior se creó con la página Monkey through Keyhole. La clase MonkeyThroughKeyholePage define un trazado mediante datos SVG y usa el constructor para cargar un mapa de bits de los recursos del 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);
        }
    }
    ...
}

Aunque el objeto keyholePath describe el contorno de una cerradura, las coordenadas son completamente arbitrarias y reflejan lo que era conveniente cuando se idearon los datos del trazado. Por este motivo, el controlador PaintSurface obtiene los límites de este trazado y llama a Translate y a Scale para moverlo al centro de la pantalla y hacerlo casi tan alto como esta:

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

Pero el trazado no se representa. En su lugar, siguiendo las transformaciones, el trazado se usa para establecer un área de recorte con esta instrucción:

canvas.ClipPath(keyholePath);

El controlador PaintSurface restablece entonces las transformaciones con una llamada a ResetMatrix y dibuja el mapa de bits para que se extienda a todo lo alto de la pantalla. En este código se supone que el mapa de bits es cuadrado, que es el caso de este mapa de bits en concreto. El mapa de bits solo se representa dentro del área definida por el trazado de recorte:

Triple screenshot of the Monkey through Keyhole page

El trazado de recorte está sujeto a las transformaciones activas cuando se llama al método ClipPath y no a las transformaciones activas cuando se muestra un objeto gráfico (como un mapa de bits). El trazado de recorte forma parte del estado del lienzo que se guarda con el método Save y se restaura con el método Restore.

Combinación de trazados de recorte

Estrictamente hablando, el área de recorte no se "define" con el método ClipPath. En cambio, se combina con el trazado de recorte existente, que comienza como un rectángulo de igual tamaño que el lienzo. Puede obtener los límites rectangulares del área de recorte mediante la propiedad LocalClipBounds o DeviceClipBounds. La propiedad LocalClipBounds devuelve un valor SKRect que refleja las transformaciones que podrían estar activas. La propiedad DeviceClipBounds devuelve un valor RectI. Se trata de un rectángulo con dimensiones de números enteros y describe el área de recorte en dimensiones de píxeles reales.

Cualquier llamada a ClipPath reduce el área de recorte, ya que la combina con una nueva área. La sintaxis completa del método ClipPath que combina el área de recorte con un rectángulo:

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

De forma predeterminada, el área de recorte resultante es una intersección del área de recorte existente y el valor SKPath o SKRect que se especifica en el método ClipPath o ClipRect. Esto se demuestra en la página Four Circles Intersect Clip. El controlador PaintSurface de la clase FourCircleInteresectClipPage reutiliza el mismo objeto SKPath para crear cuatro círculos superpuestos, cada uno de los cuales reduce el área de recorte mediante llamadas sucesivas 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);
        }
    }
}

Lo que queda es la intersección de estos cuatro círculos:

Triple screenshot of the Four Circle Intersect Clip page

La enumeración SKClipOperation solo tiene dos miembros:

  • Difference quita el trazado o el rectángulo especificados del área de recorte existente.

  • Intersect forma intersección del trazado o el rectángulo especificados con el área de recorte existente

Si reemplaza los cuatro argumentos SKClipOperation.Intersect de la clase FourCircleIntersectClipPage por SKClipOperation.Difference, verá lo siguiente:

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

Se han quitado cuatro círculos superpuestos del área de recorte.

La página Clip Operations muestra la diferencia entre estas dos operaciones con solo un par de círculos. El primer círculo de la izquierda se agrega al área de recorte con la operación de recorte predeterminada de Intersect, mientras que el segundo círculo de la derecha se agrega al área de recorte con la operación de recorte indicada por la etiqueta de texto:

Triple screenshot of the Clip Operations page

La clase ClipOperationsPage define dos objetos SKPaint como campos y, a continuación, divide la pantalla en dos áreas rectangulares. Estas áreas son diferentes en función de si el teléfono está en modo vertical u horizontal. A continuación, la clase DisplayClipOp muestra el texto y llama a ClipPath con los dos trazados de círculo para ilustrar cada operación de recorte:

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 llamada a DrawPaint normalmente hace que todo el lienzo se rellene con ese objeto SKPaint, pero en este caso, el método simplemente pinta dentro del área de recorte.

Exploración de regiones

También puede definir un área de recorte en términos de un objeto SKRegion.

Un objeto SKRegion recién creado describe un área vacía. Normalmente, la primera llamada al objeto es SetRect para que la región describa un área rectangular. El parámetro para SetRect es un valor SKRectI: un rectángulo con coordenadas de números enteros porque especifica el rectángulo en términos de píxeles. A continuación, puede llamar a SetPath con un objeto SKPath. Esta acción crea una región que es la misma que el interior del trazado, pero recortada a la región rectangular inicial.

La región también se puede modificar llamando a una de las sobrecargas del método Op, como esta:

public Boolean Op(SKRegion region, SKRegionOperation op)

La enumeración SKRegionOperation es similar a SKClipOperation, pero tiene más miembros:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

La región en la que realiza la llamada a Op se combina con la región especificada como parámetro en función del miembro SKRegionOperation. Cuando finalmente obtenga una región adecuada para el recorte, puede establecerla como área de recorte del lienzo mediante el método ClipRegion de SKCanvas:

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

En la captura de pantalla siguiente se muestran las áreas de recorte basadas en las operaciones de seis regiones. El círculo izquierdo es la región en la que se llama al método Op y el círculo derecho es la región que se pasa al método Op:

Triple screenshot of the Region Operations page

¿Estas son todas las posibilidades de combinar estos dos círculos? Considere la imagen resultante como una combinación de tres componentes, que por sí mismos se ven en las operaciones Difference, Intersect y ReverseDifference. El número total de combinaciones es de dos elevado a la tercera potencia, es decir, ocho. Las dos que faltan son la región original (que resulta de no llamar de ninguna forma a Op) y una región completamente vacía.

Es más difícil usar regiones para el recorte porque primero se debe crear un trazado y, luego, una región a partir de ese trazado y combinar varias regiones. La estructura general de la página Region Operations es muy similar a la de la página Clip Operations, pero la clase RegionOperationsPage divide la pantalla hasta en seis áreas y muestra el trabajo adicional necesario para usar regiones para este trabajo:

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

Aquí hay una gran diferencia entre los métodos ClipPath y ClipRegion:

Importante

A diferencia del método ClipPath, el método ClipRegion no se ve afectado por las transformaciones.

Para comprender la justificación de esta diferencia, resulta útil comprender lo que es una región. Si ha pensado en cómo se podrían implementar internamente las operaciones de recorte o las operaciones de regiones, es probable que le haya parecido muy complicado. Se están combinando varios trazados potencialmente muy complejos, y es probable que el contorno del trazado resultante sea una pesadilla algorítmica.

Este trabajo se simplifica considerablemente si cada trazado se reduce a una serie de líneas de barrido horizontales, como las de los televisores de tubo antiguos. Cada línea de barrido es simplemente una línea horizontal con un punto de inicio y un punto final. Por ejemplo, un círculo con un radio de 10 píxeles se puede descomponer en 20 líneas de barrido horizontales, cada una de las cuales comienza en la parte izquierda del círculo y termina en la parte derecha. Combinar dos círculos con cualquier operación de región se vuelve muy sencillo porque es simplemente una cuestión de examinar las coordenadas inicial y final de cada par de líneas de barrido correspondientes.

Esto es lo que es una región: una serie de líneas de barrido horizontales que definen un área.

Sin embargo, cuando un área se reduce a una serie de líneas de barrido, estas líneas de barrido se basan en una dimensión de píxeles determinada. En términos estrictos, la región no es un objeto gráfico vectorial. Está más cerca intrínsecamente de un mapa de bits monocromo comprimido que de un trazado. Por lo tanto, las regiones no se pueden escalar ni girar sin perder fidelidad y, por este motivo, no se transforman cuando se usan para recortar áreas.

Sin embargo, puede aplicar transformaciones a regiones para pintar. El programa Region Paint muestra de forma vívida la naturaleza interior de las regiones. La clase RegionPaintPage crea un objeto SKRegion basado en un valor SKPath de un círculo de radio de 10 unidades. Así, una transformación expande ese círculo para rellenar la 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);
            }
        }
    }
}

La llamada a DrawRegion rellena la región en naranja, mientras que la llamada a DrawPath crea el trazado original en azul para realizar la comparación:

Triple screenshot of the Region Paint page

La región es claramente una serie de coordenadas discretas.

Si no necesita usar transformaciones en conexión con las áreas de recorte, puede usar regiones para el recorte, como se muestra en la página Four-Leaf Clover. La clase FourLeafCloverPage construye una región compuesta a partir de cuatro regiones circulares, establece esa región compuesta como área de recorte y, a continuación, dibuja una serie de 360 líneas rectas que proceden del centro de la 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);
                    }
                }
            }
        }
    }
}

En realidad no parece un trébol de cuatro hojas, pero es una imagen que, de lo contrario, podría ser difícil de representar sin recortar:

Triple screenshot of the Four-Leaf Clover page