Compartir vía


Mosaico de mapa de bits SkiaSharp

Como ha visto en los dos artículos anteriores, la clase SKShader puede crear degradados lineales o circulares. Este artículo se centra en el objeto SKShader que usa un mapa de bits para representar un área en mosaico. El mapa de bits puede repetirse horizontal y verticalmente, en su orientación original o volteado alternativamente horizontal y verticalmente. El volteo evita las discontinuidades entre los mosaicos:

Ejemplo de mosaico de mapa de bits

El método estático SKShader.CreateBitmap que crea este sombreador tiene un parámetro SKBitmap y dos miembros de la enumeración SKShaderTileMode:

public static SKShader CreateBitmap (SKBitmap src, SKShaderTileMode tmx, SKShaderTileMode tmy)

Los dos parámetros indican los modos usados para el mosaico horizontal y el mosaico vertical. Se trata de la misma enumeración SKShaderTileMode que también se utiliza con los métodos de degradado.

Una sobrecarga CreateBitmap incluye un argumento SKMatrix para realizar una transformación en los mapas de bits en mosaico:

public static SKShader CreateBitmap (SKBitmap src, SKShaderTileMode tmx, SKShaderTileMode tmy, SKMatrix localMatrix)

Este artículo contiene varios ejemplos de uso de esta transformación de matriz con mapas de bits en mosaico.

Exploración de los modos de mosaico

El primer programa de la sección Bitmap Tiling de la página Shaders y otros efectos de la muestra demuestra los efectos de los dos argumentos SKShaderTileMode. El archivo XAML de Modos de volteo de mosaico de mapa de bits crea una instancia de SKCanvasView y dos vistas Picker que le permiten seleccionar un valor SKShaderTilerMode para el mosaico horizontal y vertical. Observe que una matriz de los miembros SKShaderTileMode se define en la sección Resources:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.BitmapTileFlipModesPage"
             Title="Bitmap Tile Flip Modes">

    <ContentPage.Resources>
        <x:Array x:Key="tileModes"
                 Type="{x:Type skia:SKShaderTileMode}">
            <x:Static Member="skia:SKShaderTileMode.Clamp" />
            <x:Static Member="skia:SKShaderTileMode.Repeat" />
            <x:Static Member="skia:SKShaderTileMode.Mirror" />
        </x:Array>
    </ContentPage.Resources>

    <StackLayout>
        <skiaforms:SKCanvasView x:Name="canvasView"
                                VerticalOptions="FillAndExpand"
                                PaintSurface="OnCanvasViewPaintSurface" />

        <Picker x:Name="xModePicker"
                Title="Tile X Mode"
                Margin="10, 0"
                ItemsSource="{StaticResource tileModes}"
                SelectedIndex="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged" />

        <Picker x:Name="yModePicker"
                Title="Tile Y Mode"
                Margin="10, 10"
                ItemsSource="{StaticResource tileModes}"
                SelectedIndex="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged" />

    </StackLayout>
</ContentPage>

El constructor del archivo de código subyacente se carga en el recurso de mapa de bits que muestra un mono sentado. Primero recorta la imagen utilizando el método ExtractSubset de SKBitmap para que la cabeza y los pies toquen los bordes del mapa de bits. A continuación, el constructor usa el método Resize para crear otro mapa de bits de la mitad del tamaño. Estos cambios hacen que el mapa de bits sea un poco más adecuado para el mosaico:

public partial class BitmapTileFlipModesPage : ContentPage
{
    SKBitmap bitmap;

    public BitmapTileFlipModesPage ()
    {
        InitializeComponent ();

        SKBitmap origBitmap = BitmapExtensions.LoadBitmapResource(
            GetType(), "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");

        // Define cropping rect
        SKRectI cropRect = new SKRectI(5, 27, 296, 260);

        // Get the cropped bitmap
        SKBitmap croppedBitmap = new SKBitmap(cropRect.Width, cropRect.Height);
        origBitmap.ExtractSubset(croppedBitmap, cropRect);

        // Resize to half the width and height
        SKImageInfo info = new SKImageInfo(cropRect.Width / 2, cropRect.Height / 2);
        bitmap = croppedBitmap.Resize(info, SKBitmapResizeMethod.Box);
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Get tile modes from Pickers
        SKShaderTileMode xTileMode =
            (SKShaderTileMode)(xModePicker.SelectedIndex == -1 ?
                                        0 : xModePicker.SelectedItem);
        SKShaderTileMode yTileMode =
            (SKShaderTileMode)(yModePicker.SelectedIndex == -1 ?
                                        0 : yModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(bitmap, xTileMode, yTileMode);
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

El controlador PaintSurface obtiene la configuración SKShaderTileMode de las dos vistas Picker y crea un objeto SKShader basado en el mapa de bits y esos dos valores. Este sombreador se usa para rellenar el lienzo:

Modos de volteo de mosaico de mapa de bits

La pantalla de iOS de la izquierda muestra el efecto de los valores predeterminados de SKShaderTileMode.Clamp. El mapa de bits se encuentra en la esquina superior izquierda. Debajo del mapa de bits, la fila inferior de píxeles se repite hasta abajo. A la derecha del mapa de bits, la columna de píxeles situada más a la derecha se repite en todo el recorrido. El resto del lienzo está coloreado por el píxel marrón oscuro en la esquina inferior derecha del mapa de bits. Debe ser obvio que la opción Clamp casi nunca se usa con la representación en mosaico del mapa de bits.

La pantalla de Android del centro muestra el resultado de SKShaderTileMode.Repeat para ambos argumentos. El mosaico se repite horizontal y verticalmente. La pantalla de la Plataforma universal de Windows muestra SKShaderTileMode.Mirror. Los mosaicos se repiten pero se voltean horizontal y verticalmente. La ventaja de esta opción es que no hay discontinuidades entre los mosaicos.

Tenga en cuenta que puede usar diferentes opciones para la repetición horizontal y vertical. Puede especificar SKShaderTileMode.Mirror como segundo argumento para CreateBitmap pero SKShaderTileMode.Repeat como tercer argumento. En cada fila, los monos siguen alternando entre la imagen normal y la imagen reflejada, pero ninguno de los monos está al revés.

Fondos con patrones

El mosaico de mapa de bits se usa normalmente para crear un fondo con patrones a partir de un mapa de bits relativamente pequeño. El ejemplo clásico es una pared de ladrillo.

La página Muro de ladrillo algorítmico crea un pequeño mapa de bits que se asemeja a un ladrillo entero y a dos mitades de ladrillo separadas por mortero. Dado que este ladrillo también se usa en el ejemplo siguiente, se crea mediante un constructor estático y se hace público con una propiedad estática:

public class AlgorithmicBrickWallPage : ContentPage
{
    static AlgorithmicBrickWallPage()
    {
        const int brickWidth = 64;
        const int brickHeight = 24;
        const int morterThickness = 6;
        const int bitmapWidth = brickWidth + morterThickness;
        const int bitmapHeight = 2 * (brickHeight + morterThickness);

        SKBitmap bitmap = new SKBitmap(bitmapWidth, bitmapHeight);

        using (SKCanvas canvas = new SKCanvas(bitmap))
        using (SKPaint brickPaint = new SKPaint())
        {
            brickPaint.Color = new SKColor(0xB2, 0x22, 0x22);

            canvas.Clear(new SKColor(0xF0, 0xEA, 0xD6));
            canvas.DrawRect(new SKRect(morterThickness / 2,
                                       morterThickness / 2,
                                       morterThickness / 2 + brickWidth,
                                       morterThickness / 2 + brickHeight),
                                       brickPaint);

            int ySecondBrick = 3 * morterThickness / 2 + brickHeight;

            canvas.DrawRect(new SKRect(0,
                                       ySecondBrick,
                                       bitmapWidth / 2 - morterThickness / 2,
                                       ySecondBrick + brickHeight),
                                       brickPaint);

            canvas.DrawRect(new SKRect(bitmapWidth / 2 + morterThickness / 2,
                                       ySecondBrick,
                                       bitmapWidth,
                                       ySecondBrick + brickHeight),
                                       brickPaint);
        }

        // Save as public property for other programs
        BrickWallTile = bitmap;
    }

    public static SKBitmap BrickWallTile { private set; get; }
    ···
}

El mapa de bits resultante es de 70 píxeles de ancho y 60 píxeles de alto:

Mosaico de pared de ladrillo algorítmico

El resto de la página Muro de ladrillo algorítmico crea un objeto SKShader que repite esta imagen horizontal y verticalmente:

public class AlgorithmicBrickWallPage : ContentPage
{
    ···
    public AlgorithmicBrickWallPage ()
    {
        Title = "Algorithmic Brick Wall";

        // Create SKCanvasView
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(BrickWallTile,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Este es el resultado:

Muro de ladrillos algorítmicos

Es posible que prefiera algo un poco más realista. En ese caso, puede tomar una fotografía de una pared de ladrillo real y luego recortarla. Este mapa de bits es de 300 píxeles de ancho y 150 píxeles de alto:

Mosaico de pared de ladrillo

Este mapa de bits se usa en la página Muro de ladrillo fotografico.

public class PhotographicBrickWallPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(PhotographicBrickWallPage),
                        "SkiaSharpFormsDemos.Media.BrickWallTile.jpg");

    public PhotographicBrickWallPage()
    {
        Title = "Photographic Brick Wall";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Observa que los argumentos SKShaderTileMode de CreateBitmap son ambos Mirror. Esta opción suele ser necesaria cuando se usan mosaicos creados a partir de imágenes reales. La creación de reflejo de los mosaicos evita las discontinuidades:

Pared de ladrillo fotográfico

Se requiere algún trabajo para obtener un mapa de bits adecuado para el mosaico. Este no funciona muy bien porque el ladrillo más oscuro destaca demasiado. Aparece regularmente en las imágenes repetidas, revelando el hecho de que esta pared de ladrillo se construyó a partir de un mapa de bits más pequeño.

La carpeta Media del ejemplo también incluye esta imagen de una pared de piedra:

Mosaico de pared de piedra

Sin embargo, el mapa de bits original es demasiado grande para un mosaico. Se podría redimensionar, pero el método SKShader.CreateBitmap también puede redimensionar el mosaico aplicándole una transformación. Esta opción se muestra en la página Muro de piedra:

public class StoneWallPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(StoneWallPage),
                        "SkiaSharpFormsDemos.Media.StoneWallTile.jpg");

    public StoneWallPage()
    {
        Title = "Stone Wall";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            // Create scale transform
            SKMatrix matrix = SKMatrix.MakeScale(0.5f, 0.5f);

            // Create bitmap tiling
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror,
                                                 matrix);
            // Draw background
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Se crea un valor SKMatrix para escalar la imagen a la mitad de su tamaño original:

Pared de piedra

¿Funciona la transformación en el mapa de bits original usado en el método CreateBitmap? ¿O transforma la matriz resultante de mosaicos?

Una manera fácil de responder a esta pregunta es incluir una rotación como parte de la transformación:

SKMatrix matrix = SKMatrix.MakeScale(0.5f, 0.5f);
SKMatrix.PostConcat(ref matrix, SKMatrix.MakeRotationDegrees(15));

Si la transformación se aplica al mosaico individual, cada imagen repetida del mosaico debe girarse y el resultado contendrá muchas discontinuidades. Pero es obvio en esta captura de pantalla que se transforma la matriz compuesta de mosaicos:

Pared de piedra girada

En la sección Alineación de mosaicos, verá un ejemplo de una transformación de traslación aplicada al sombreador.

El ejemplo simula un fondo de grano de madera mediante mosaicos de mapa de bits basado en este mapa de bits cuadrado de 240 píxeles:

Grano de madera

Esa es una fotografía de un suelo de madera. La opción SKShaderTileMode.Mirror permite que aparezca como un área de madera mucho mayor:

Reloj cat

Alineación de mosaicos

Todos los ejemplos mostrados hasta ahora han usado el sombreador creado por SKShader.CreateBitmap para cubrir todo el lienzo. En la mayoría de los casos, usará el mosaico de mapa de bits para archivar áreas más pequeñas o (más rara vez) para rellenar los interiores de líneas gruesas. Este es el mosaico fotográfico del muro de ladrillo usado para un rectángulo más pequeño:

Alineación de mosaicos

Esto podría parecerle bien, o quizás no. Tal vez le moleste que el patrón de mosaicos no empiece con un ladrillo completo en la esquina superior izquierda del rectángulo. Esto se debe a que los sombreadores están alineados con el lienzo y no con el objeto gráfico que adornan.

La corrección es sencilla. Cree un valor SKMatrix basado en una transformación de traslación. La transformación desplaza eficazmente el patrón en mosaico al punto en el que desea que se alinee la esquina superior izquierda del mosaico. Este enfoque se demuestra en la página Alineación de mosaicos, que creó la imagen de los mosaicos no alineados que se muestra arriba:

public class TileAlignmentPage : ContentPage
{
    bool isAligned;

    public TileAlignmentPage()
    {
        Title = "Tile Alignment";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;

        // Add tap handler
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            isAligned ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);

        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            SKRect rect = new SKRect(info.Width / 7,
                                     info.Height / 7,
                                     6 * info.Width / 7,
                                     6 * info.Height / 7);

            // Get bitmap from other program
            SKBitmap bitmap = AlgorithmicBrickWallPage.BrickWallTile;

            // Create bitmap tiling
            if (!isAligned)
            {
                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat);
            }
            else
            {
                SKMatrix matrix = SKMatrix.MakeTranslation(rect.Left, rect.Top);

                paint.Shader = SKShader.CreateBitmap(bitmap,
                                                     SKShaderTileMode.Repeat,
                                                     SKShaderTileMode.Repeat,
                                                     matrix);
            }

            // Draw rectangle
            canvas.DrawRect(rect, paint);
        }
    }
}

La página Alineación de mosaicos incluye un TapGestureRecognizer. Pulse o haga clic en la pantalla y el programa cambiará al método SKShader.CreateBitmap con un argumento SKMatrix. Esta transformación desplaza el patrón para que la esquina superior izquierda contenga un ladrillo completo:

Alineación de iconos pulsada

También puede utilizar esta técnica para asegurarse de que el patrón de mapa de bits en mosaico está centrado dentro del área que pinta. En la página Mosaicos centrados, el controlador PaintSurface calcula primero las coordenadas como si fuera a mostrar el mapa de bits único en el centro del lienzo. A continuación, usa esas coordenadas para crear una transformación de traslación para SKShader.CreateBitmap. Esta transformación desplaza todo el patrón para que un mosaico se centre:

public class CenteredTilesPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(CenteredTilesPage),
                        "SkiaSharpFormsDemos.Media.monkey.png");

    public CenteredTilesPage ()
    {
        Title = "Centered Tiles";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Find coordinates to center bitmap in canvas...
        float x = (info.Width - bitmap.Width) / 2f;
        float y = (info.Height - bitmap.Height) / 2f;

        using (SKPaint paint = new SKPaint())
        {
            // ... but use them to create a translate transform
            SKMatrix matrix = SKMatrix.MakeTranslation(x, y);
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat,
                                                 matrix);

            // Use that tiled bitmap pattern to fill a circle
            canvas.DrawCircle(info.Rect.MidX, info.Rect.MidY,
                              Math.Min(info.Width, info.Height) / 2,
                              paint);
        }
    }
}

El controlador PaintSurface concluye dibujando un círculo en el centro del lienzo. Efectivamente, uno de los mosaicos está exactamente en el centro del círculo y los demás están dispuestos de forma simétrica:

Iconos centrados

Otro enfoque de centrado es en realidad un poco más fácil. En lugar de construir una transformación de traslación que coloque un mosaico en el centro, puede centrar una esquina del patrón en mosaico. En la llamada SKMatrix.MakeTranslation, use argumentos para el centro del lienzo:

SKMatrix matrix = SKMatrix.MakeTranslation(info.Rect.MidX, info.Rect.MidY);

El patrón sigue centrado y simétrico, pero no hay ningún mosaico en el centro:

Iconos centrados alternativos

Simplificación mediante la rotación

A veces, el uso de una transformación de rotación en el método SKShader.CreateBitmap puede simplificar el mosaico del mapa de bits. Esto resulta evidente al intentar definir un mosaico para una valla de alambre. El archivo ChainLinkTile.cs crea el mosaico que se muestra aquí (con un fondo rosa con fines de claridad):

Icono de vínculo de cadena duro

El mosaico debe incluir dos vínculos para que el código divida el mosaico en cuatro cuadrantes. Los cuadrantes superior izquierdo e inferior derecho son iguales, pero no están completos. Los alambres tienen pequeñas muescas que deben manipularse con algún dibujo adicional en los cuadrantes superior derecho e inferior izquierdo. El archivo que hace todo este trabajo tiene 174 líneas.

Resulta mucho más fácil crear este mosaico:

Icono de vínculo de cadena más sencillo

Si el sombreador de mosaicos de mapa de bits gira 90 grados, los objetos visuales son casi los mismos.

El código para crear el mosaico de alambre más fácil forma parte de la página Mosaico de alambres. El constructor determina un tamaño de mosaico basado en el tipo de dispositivo en el que se está ejecutando el programa y, a continuación, llama a CreateChainLinkTile, que dibuja en el mapa de bits utilizando líneas, trazados y sombreadores de degradado:

public class ChainLinkFencePage : ContentPage
{
    ···
    SKBitmap tileBitmap;

    public ChainLinkFencePage ()
    {
        Title = "Chain-Link Fence";

        // Create bitmap for chain-link tiling
        int tileSize = Device.Idiom == TargetIdiom.Desktop ? 64 : 128;
        tileBitmap = CreateChainLinkTile(tileSize);

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    SKBitmap CreateChainLinkTile(int tileSize)
    {
        tileBitmap = new SKBitmap(tileSize, tileSize);
        float wireThickness = tileSize / 12f;

        using (SKCanvas canvas = new SKCanvas(tileBitmap))
        using (SKPaint paint = new SKPaint())
        {
            canvas.Clear();
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = wireThickness;
            paint.IsAntialias = true;

            // Draw straight wires first
            paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                         new SKPoint(0, tileSize),
                                                         new SKColor[] { SKColors.Silver, SKColors.Black },
                                                         new float[] { 0.4f, 0.6f },
                                                         SKShaderTileMode.Clamp);

            canvas.DrawLine(0, tileSize / 2,
                            tileSize / 2, tileSize / 2 - wireThickness / 2, paint);

            canvas.DrawLine(tileSize, tileSize / 2,
                            tileSize / 2, tileSize / 2 + wireThickness / 2, paint);

            // Draw curved wires
            using (SKPath path = new SKPath())
            {
                path.MoveTo(tileSize / 2, 0);
                path.LineTo(tileSize / 2 - wireThickness / 2, tileSize / 2);
                path.ArcTo(wireThickness / 2, wireThickness / 2,
                           0,
                           SKPathArcSize.Small,
                           SKPathDirection.CounterClockwise,
                           tileSize / 2, tileSize / 2 + wireThickness / 2);

                paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                             new SKPoint(0, tileSize),
                                                             new SKColor[] { SKColors.Silver, SKColors.Black },
                                                             null,
                                                             SKShaderTileMode.Clamp);
                canvas.DrawPath(path, paint);

                path.Reset();
                path.MoveTo(tileSize / 2, tileSize);
                path.LineTo(tileSize / 2 + wireThickness / 2, tileSize / 2);
                path.ArcTo(wireThickness / 2, wireThickness / 2,
                           0,
                           SKPathArcSize.Small,
                           SKPathDirection.CounterClockwise,
                           tileSize / 2, tileSize / 2 - wireThickness / 2);

                paint.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0),
                                                             new SKPoint(0, tileSize),
                                                             new SKColor[] { SKColors.White, SKColors.Silver },
                                                             null,
                                                             SKShaderTileMode.Clamp);
                canvas.DrawPath(path, paint);
            }
            return tileBitmap;
        }
    }
    ···
}

Excepto por los alambres, el mosaico es transparente, lo que significa que puede mostrarlo encima de otra cosa. El programa se carga en uno de los recursos de mapa de bits, lo muestra para rellenar el lienzo y, a continuación, dibuja el sombreador en la parte superior:

public class ChainLinkFencePage : ContentPage
{
    SKBitmap monkeyBitmap = BitmapExtensions.LoadBitmapResource(
        typeof(ChainLinkFencePage), "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");
    ···

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.UniformToFill,
                            BitmapAlignment.Center, BitmapAlignment.Start);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(tileBitmap,
                                                 SKShaderTileMode.Repeat,
                                                 SKShaderTileMode.Repeat,
                                                 SKMatrix.MakeRotationDegrees(45));
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Observe que el sombreador gira 45 grados, de modo que está orientado como una valla de alambres real:

Barrera de vínculo de cadena

Animación de mosaicos de mapas de bits

Puede animar un patrón de mosaico de mapa de bits completo animando la transformación de matriz. Tal vez desee que el patrón se mueva horizontal o verticalmente, o ambas cosas. Puede hacerlo mediante la creación de una transformación de traslación basada en las coordenadas de desplazamiento.

También es posible dibujar en un mapa de bits pequeño, o manipular los bits de píxel del mapa de bits a una velocidad de 60 veces por segundo. Ese mapa de bits se puede usar para la representación en mosaico y todo el patrón en mosaico puede parecer animado.

En la página Mosaico de mapa de bits animado se muestra este enfoque. Se crea una instancia de un mapa de bits como un campo cuadrado de 64 píxeles. El constructor llama a DrawBitmap para darle una apariencia inicial. Si el campo angle es cero (como cuando se llama al método por primera vez), el mapa de bits contiene dos líneas cruzadas como una X. Las líneas se hacen lo suficientemente largas como para llegar siempre al borde del mapa de bits, independientemente del valor angle:

public class AnimatedBitmapTilePage : ContentPage
{
    const int SIZE = 64;

    SKCanvasView canvasView;
    SKBitmap bitmap = new SKBitmap(SIZE, SIZE);
    float angle;
    ···

    public AnimatedBitmapTilePage ()
    {
        Title = "Animated Bitmap Tile";

        // Initialize bitmap prior to animation
        DrawBitmap();

        // Create SKCanvasView
        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ···
    void DrawBitmap()
    {
        using (SKCanvas canvas = new SKCanvas(bitmap))
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = SIZE / 8;

            canvas.Clear();
            canvas.Translate(SIZE / 2, SIZE / 2);
            canvas.RotateDegrees(angle);
            canvas.DrawLine(-SIZE, -SIZE, SIZE, SIZE, paint);
            canvas.DrawLine(-SIZE, SIZE, SIZE, -SIZE, paint);
        }
    }
    ···
}

La sobrecarga de animación se produce en las invalidaciones OnAppearing y OnDisappearing. El método OnTimerTick anima el valor angle de 0 grados a 360 grados cada 10 segundos para girar la figura X en el mapa de bits:

public class AnimatedBitmapTilePage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    Stopwatch stopwatch = new Stopwatch();
    ···

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 10;     // seconds
        angle = (float)(360f * (stopwatch.Elapsed.TotalSeconds % duration) / duration);
        DrawBitmap();
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

Debido a la simetría de la figura X, esto es lo mismo que girar el valor angle de 0 grados a 90 grados cada 2,5 segundos.

El controlador PaintSurface crea un sombreador a partir del mapa de bits y usa el objeto paint para colorear todo el lienzo:

public class AnimatedBitmapTilePage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateBitmap(bitmap,
                                                 SKShaderTileMode.Mirror,
                                                 SKShaderTileMode.Mirror);
            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Las opciones SKShaderTileMode.Mirror garantizan que los brazos de la X en cada mapa de bits se unan a la X en los mapas de bits adyacentes para crear un patrón animado general que parece mucho más complejo de lo que sugeriría la simple animación:

Icono de mapa de bits animado