Recorte con trazados y regiones

Descargar ejemplo Descarga del ejemplo

Usar rutas de acceso para recortar gráficos en áreas específicas y crear regiones

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

Mono a través de un agujero de llave

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 SKPath objeto, pero también puede definir un área de recorte mediante un SKRegion objeto . Estos dos tipos de objetos al principio parecen relacionados porque puede crear una región a partir de una ruta de acceso. Sin embargo, no se puede crear una ruta de acceso desde una región y son muy diferentes internamente: una ruta de acceso consta de una serie de líneas y curvas, mientras que una región se define mediante una serie de líneas de examen horizontales.

La imagen anterior fue creada por la página Monkey a través de Keyhole . La MonkeyThroughKeyholePage clase define una ruta de acceso 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 keyholePath objeto describe el contorno de un agujero de llaves, las coordenadas son completamente arbitrarias y reflejan lo que era conveniente cuando se diseñaron los datos de ruta de acceso. Por este motivo, el PaintSurface controlador obtiene los límites de esta ruta de acceso y llama Translate a y Scale para mover la ruta de acceso al centro de la pantalla y para que sea casi tan alto como la pantalla:

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 la ruta de acceso no se representa. En su lugar, después de las transformaciones, la ruta de acceso se usa para establecer un área de recorte con esta instrucción:

canvas.ClipPath(keyholePath);

A PaintSurface continuación, el controlador restablece las transformaciones con una llamada a ResetMatrix y dibuja el mapa de bits para extenderse al alto completo de la pantalla. En este código se supone que el mapa de bits es cuadrado, que es este mapa de bits determinado. El mapa de bits solo se representa dentro del área definida por la ruta de recorte:

Captura de pantalla triple de la página Mono a través de Keyhole

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

Combinación de rutas de recorte

Estrictamente hablando, el área de recorte no está "establecida" por el ClipPath método . En su lugar, se combina con la ruta de recorte existente, que comienza como un rectángulo igual de tamaño al lienzo. Puede obtener los límites rectangulares del área de recorte mediante la LocalClipBounds propiedad o la DeviceClipBounds propiedad . La LocalClipBounds propiedad devuelve un SKRect valor que refleja las transformaciones que podrían estar en vigor. La DeviceClipBounds propiedad devuelve un RectI valor. Se trata de un rectángulo con dimensiones enteras y describe el área de recorte en dimensiones de píxeles reales.

Cualquier llamada para ClipPath reducir el área de recorte combinando el área de recorte con un área nueva. Sintaxis completa del ClipPath método 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 o SKPathSKRect que se especifica en el ClipPath método o ClipRect . Esto se muestra en la página De intersección de cuatro círculos. El PaintSurface controlador de la FourCircleInteresectClipPage clase reutiliza el mismo SKPath objeto para crear cuatro círculos superpuestos, cada uno de los cuales reduce el área de recorte a través de 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:

Captura de pantalla triple de la página Clip de intersección de cuatro círculos

La SKClipOperation enumeración solo tiene dos miembros:

  • Difference quita la ruta de acceso o el rectángulo especificados del área de recorte existente.

  • Intersect intersecta la ruta de acceso o rectángulo especificados con el área de recorte existente.

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

Captura de pantalla triple de la página De intersección de cuatro círculos con una operación de diferencia

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

La página Operaciones de recorte ilustra 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 , 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 Intersecttexto:

Captura de pantalla triple de la página Operaciones de recorte

La ClipOperationsPage clase define dos SKPaint objetos 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 o horizontal. A DisplayClipOp continuación, la clase muestra el texto y llama a ClipPath con las dos rutas de acceso 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();
}

Llamar DrawPaint normalmente hace que todo el lienzo se rellene con ese SKPaint objeto, 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 SKRegion objeto .

Un objeto recién creado SKRegion 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 SKRectI valor : un rectángulo con coordenadas enteras porque especifica el rectángulo en términos de píxeles. A continuación, puede llamar a SetPath con un SKPath objeto . Esto crea una región que es la misma que el interior de la ruta de acceso, pero recortada en la región rectangular inicial.

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

public Boolean Op(SKRegion region, SKRegionOperation op)

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

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

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

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

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

Captura de pantalla triple de la página Operaciones de región

¿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 Differenceoperaciones , Intersecty ReverseDifference . El número total de combinaciones es de dos a la tercera potencia, u ocho. Los dos que faltan son la región original (que resulta de no llamar Op a en absoluto) y una región completamente vacía.

Es más difícil usar regiones para recortar porque primero debe crear una ruta de acceso y, a continuación, una región de esa ruta de acceso y, a continuación, combinar varias regiones. La estructura general de la página Operaciones de región es muy similar a Las operaciones de recorte , pero la RegionOperationsPage clase divide la pantalla 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();
        }
    }
}

Esta es una gran diferencia entre el ClipPath método y el ClipRegion método :

Importante

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

Para comprender la lógica de esta diferencia, resulta útil comprender lo que es una región. Si ha pensado en cómo se pueden implementar internamente las operaciones de recorte o las operaciones de región, es probable que parezca muy complicado. Se combinan varias rutas de acceso potencialmente muy complejas y es probable que el esquema de la ruta resultante sea una pesadilla algorítmica.

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

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

Sin embargo, cuando un área se reduce a una serie de líneas de examen, estas líneas de examen se basan en una dimensión de píxel determinada. Estrictamente hablando, la región no es un objeto gráfico vectorial. Está más cerca de la naturaleza a un mapa de bits monocromático comprimido que a una ruta de acceso. 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 con fines de pintura. El programa Region Paint muestra vívidamente la naturaleza interna de las regiones. La RegionPaintPage clase crea un SKRegion objeto basado en un SKPath círculo de radio de 10 unidades. Después, 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 DrawRegion llamada rellena la región en naranja, mientras que la DrawPath llamada tiza la ruta de acceso original en azul para la comparación:

Captura de pantalla triple de la página Pintura de región

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

Si no es necesario usar transformaciones en relación con las áreas de recorte, puede usar regiones para recortar, como se muestra en la página De conmutación de cuatro hojas . La FourLeafCloverPage clase 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);
                    }
                }
            }
        }
    }
}

Realmente no parece un clover de cuatro hojas, pero es una imagen que, de lo contrario, podría ser difícil de representar sin recortar:

Captura de pantalla triple de la página Four-Leaf Clover