Degradado lineal SkiaSharp

La clase SKPaint define una propiedad Color que se usa para trazar líneas o rellenar áreas con un color sólido. También puedes trazar líneas o rellenar áreas con degradados, que son combinaciones graduales de colores:

Ejemplo de degradado lineal

El tipo de degradado más básico es un degradado lineal. La combinación de colores se produce en una línea (denominada línea degradada) de un punto a otro. Las líneas que son perpendiculares a la línea de degradado tienen el mismo color. Se crea un degradado lineal mediante uno de los dos métodos estáticos SKShader.CreateLinearGradient. La diferencia entre las dos sobrecargas es que una incluye una transformación de matriz y la otra no.

Estos métodos devuelven un objeto de tipo SKShader que se establece en la propiedad Shader de SKPaint. Si la propiedad Shader no es null, invalida la propiedad Color. Cualquier línea que esté trazada o cualquier área que esté rellenada con este objeto SKPaint se basa en el degradado en lugar del color sólido.

Nota:

La propiedad Shader se omite cuando se incluye un objeto SKPaint en una llamada DrawBitmap. Puedes usar la propiedad Color de SKPaint para establecer un nivel de transparencia a fin de mostrar un mapa de bits (como se describe en el artículo Visualización de mapas de bits de SkiaSharp), pero no puedes usar la propiedad Shader para mostrar un mapa de bits con una transparencia de degradado. Hay otras técnicas disponibles para mostrar mapas de bits con transparencias de degradado: estos se describen en los artículos Degradados circulares de SkiaSharp y Modos de composición y fusión de SkiaSharp.

Degradados de esquina a esquina

A menudo, un degradado lineal se extiende desde una esquina de un rectángulo a otra. Si el punto inicial es la esquina superior izquierda del rectángulo, el degradado puede ampliarse:

  • verticalmente a la esquina inferior izquierda
  • horizontalmente a la esquina superior derecha
  • diagonalmente a la esquina inferior derecha

El degradado lineal diagonal se muestra en la primera página de la sección Sombreadores SkiaSharp y otros efectos de la muestra. La página Degradado de esquina a esquina crea un elemento SKCanvasView en su constructor. El controlador PaintSurface crea un objeto SKPaint en una instrucción using y, después, define un rectángulo cuadrado de 300 píxeles centrado en el lienzo:

public class CornerToCornerGradientPage : ContentPage
{
    ···
    public CornerToCornerGradientPage ()
    {
        Title = "Corner-to-Corner Gradient";

        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 300-pixel square centered rectangle
            float x = (info.Width - 300) / 2;
            float y = (info.Height - 300) / 2;
            SKRect rect = new SKRect(x, y, x + 300, y + 300);

            // Create linear gradient from upper-left to lower-right
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Left, rect.Top),
                                new SKPoint(rect.Right, rect.Bottom),
                                new SKColor[] { SKColors.Red, SKColors.Blue },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Repeat);

            // Draw the gradient on the rectangle
            canvas.DrawRect(rect, paint);
            ···
        }
    }
}

A la propiedad Shader de SKPaint se le asigna el valor devuelto SKShader del método estático SKShader.CreateLinearGradient. Los cinco argumentos son los siguientes:

  • El punto inicial del degradado, establecido aquí en la esquina superior izquierda del rectángulo.
  • El punto final del degradado, establecido aquí en la esquina inferior derecha del rectángulo.
  • Una matriz de dos o más colores que contribuyen al degradado.
  • Una matriz de valores float que indica la posición relativa de los colores dentro de la línea de degradado.
  • Un miembro de la enumeración SKShaderTileMode que indica cómo se comporta el degradado más allá de los extremos de la línea de degradado.

Una vez creado el objeto degradado, el método DrawRect dibuja el rectángulo cuadrado de 300 píxeles mediante el objeto SKPaint que incluye el sombreador. Aquí se ejecuta en iOS, Android y la Plataforma universal de Windows (UWP):

Degradado de esquina a esquina

La línea de degradado se define mediante los dos puntos especificados como los dos primeros argumentos. Observa que estos puntos hacen referencia al lienzo y no al objeto gráfico mostrado con el degradado. A lo largo de la línea de degradado, el color cambia gradualmente de rojo en la parte superior izquierda a azul en la parte inferior derecha. Cualquier línea que sea perpendicular a la línea degradada tiene un color constante.

La matriz de valores float especificados como cuarto argumento tiene una correspondencia uno a uno con la matriz de colores. Los valores indican la posición relativa a lo largo de la línea de degradado donde se producen esos colores. Aquí, el 0 significa que Red se produce al principio de la línea de degradado y 1 significa que Blue se produce al final de la línea. Los números deben ser ascendentes y deben estar en el intervalo entre 0 y 1. Si no están en ese intervalo, se ajustarán para que lo estén.

Los dos valores de la matriz se pueden establecer en algo que no sea 0 y 1. Pruebe esto:

new float[] { 0.25f, 0.75f }

Ahora, todo el primer cuarto de la línea degradada es rojo puro y el último cuarto es azul puro. La mezcla de rojo y azul está restringida a la mitad central de la línea degradada.

Por lo general, querrás espaciar estos valores de posición de 0 a 1. Si ese es el caso, simplemente puedes proporcionar null como cuarto argumento a CreateLinearGradient.

Aunque este degradado se define entre dos esquinas del rectángulo cuadrado de 300 píxeles, no está restringido a rellenar ese rectángulo. La página Degradado de esquina a esquina incluye código adicional que responde a pulsaciones o clics del mouse en la página. El campo drawBackground se alterna entre true y false con cada pulsación. Si el valor es true, el controlador PaintSurface usa el mismo objeto SKPaint para rellenar todo el lienzo y, después, dibuja un rectángulo negro que indica el rectángulo más pequeño:

public class CornerToCornerGradientPage : ContentPage
{
    bool drawBackground;

    public CornerToCornerGradientPage ()
    {
        ···
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            drawBackground ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPaint paint = new SKPaint())
        {
            ···
            if (drawBackground)
            {
                // Draw the gradient on the whole canvas
                canvas.DrawRect(info.Rect, paint);

                // Outline the smaller rectangle
                paint.Shader = null;
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                canvas.DrawRect(rect, paint);
            }
        }
    }
}

Esto es lo que verás después de pulsar la pantalla:

Degradado de esquina a esquina completa

Observa que el degradado se repite en el mismo patrón más allá de los puntos que definen la línea de degradado. Esta repetición se produce porque el último argumento de CreateLinearGradient es SKShaderTileMode.Repeat. (Verás las otras opciones en breve).

Observa también que los puntos que se usan para especificar la línea de degradado no son únicos. Las líneas que son perpendiculares a la línea de degradado tienen el mismo color, por lo que hay un número infinito de líneas de degradado que se pueden especificar para el mismo efecto. Por ejemplo, al rellenar un rectángulo con un degradado horizontal, puedes especificar las esquinas superior izquierda y superior derecha, o las esquinas inferior izquierda e inferior derecha, o dos puntos cualesquiera que estén a la misma altura que esas líneas y que sean paralelos.

Experimento interactivo

Puedes experimentar interactivamente con degradados lineales mediante la página Degradado lineal interactivo. En esta página se usa la clase InteractivePage introducida en el artículo Tres formas de dibujar un arco. InteractivePage controla los eventos TouchEffect para mantener una colección de objetos TouchPoint que se pueden mover con los dedos o el mouse.

El archivo XAML adjunta el objeto TouchEffect a un elemento primario de SKCanvasView y también incluye un objeto Picker que permite seleccionar uno de los tres miembros de la enumeración SKShaderTileMode:

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
                       xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Effects.InteractiveLinearGradientPage"
                       Title="Interactive Linear Gradient">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid BackgroundColor="White"
              Grid.Row="0">
            <skiaforms:SKCanvasView x:Name="canvasView"
                                    PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

        <Picker x:Name="tileModePicker"
                Grid.Row="1"
                Title="Shader Tile Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array 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>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</local:InteractivePage>

El constructor del archivo de código subyacente crea dos objetos TouchPoint para los puntos inicial y final del degradado lineal. El controlador PaintSurface define una matriz de tres colores (para un degradado de rojo a verde a azul) y obtiene el objeto actual SKShaderTileMode a partir de Picker:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    public InteractiveLinearGradientPage ()
    {
        InitializeComponent ();

        touchPoints = new TouchPoint[2];

        for (int i = 0; i < 2; i++)
        {
            touchPoints[i] = new TouchPoint
            {
                Center = new SKPoint(100 + i * 200, 100 + i * 200)
            };
        }

        InitializeComponent();
        baseCanvasView = canvasView;
    }

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

        SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
        SKShaderTileMode tileMode =
            (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
                                        0 : tileModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateLinearGradient(touchPoints[0].Center,
                                                         touchPoints[1].Center,
                                                         colors,
                                                         null,
                                                         tileMode);
            canvas.DrawRect(info.Rect, paint);
        }
        ···
    }
}

El controlador PaintSurface crea el objeto SKShader a partir de toda esa información y lo usa para colorear todo el lienzo. La matriz de valores float se establece en null. De lo contrario, para que los tres colores ocupen el mismo espacio, establecería ese parámetro en una matriz con los valores 0, 0,5 y 1.

La mayor parte del controlador PaintSurface se dedica a mostrar varios objetos: los puntos táctiles como círculos de contorno, la línea de degradado y las líneas perpendiculares a las líneas de degradado en los puntos táctiles:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // Display the touch points here rather than by TouchPoint
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 3;

            foreach (TouchPoint touchPoint in touchPoints)
            {
                canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
            }

            // Draw gradient line connecting touchpoints
            canvas.DrawLine(touchPoints[0].Center, touchPoints[1].Center, paint);

            // Draw lines perpendicular to the gradient line
            SKPoint vector = touchPoints[1].Center - touchPoints[0].Center;
            float length = (float)Math.Sqrt(Math.Pow(vector.X, 2) +
                                            Math.Pow(vector.Y, 2));
            vector.X /= length;
            vector.Y /= length;
            SKPoint rotate90 = new SKPoint(-vector.Y, vector.X);
            rotate90.X *= 200;
            rotate90.Y *= 200;

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center - rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center - rotate90,
                            paint);
        }
    }
}

La línea de degradado que conecta los dos puntos táctiles es fácil de dibujar, pero las líneas perpendiculares requieren más trabajo. La línea de degradado se convierte en un vector, normalizado para tener una longitud de una unidad y, después, gira 90 grados. Ese vector tiene una longitud de 200 píxeles. Se usa para dibujar cuatro líneas que se extienden desde los puntos táctiles con el fin de que sean perpendiculares a la línea de degradado.

Las líneas perpendiculares coinciden con el principio y el final del degradado. Lo que sucede más allá de esas líneas depende del valor de la enumeración SKShaderTileMode:

Degradado lineal interactivo

En las tres capturas de pantalla se muestran los resultados de los tres valores diferentes de SKShaderTileMode. La captura de pantalla de iOS muestra SKShaderTileMode.Clamp, que simplemente extiende los colores en el borde del degradado. La opción SKShaderTileMode.Repeat de la captura de pantalla de Android muestra cómo se repite el patrón de degradado. La opción SKShaderTileMode.Mirror de la captura de pantalla de UWP también repite el patrón, pero el patrón se invierte cada vez, lo que no da lugar a discontinuidades de color.

Degradados en degradados

La clase SKShader no define propiedades o métodos públicos, excepto para Dispose. Por lo tanto, los objetos SKShader que crean sus métodos estáticos son inmutables. Incluso si usas el mismo degradado para dos objetos diferentes, es probable que quieras variar ligeramente el degradado. Para ello, deberás crear un objeto SKShader.

La página Texto degradado muestra texto y un primer plano que se colorean con degradados similares:

Texto degradado

Las únicas diferencias en los degradados son los puntos iniciales y finales. El degradado utilizado para mostrar texto se basa en dos puntos en las esquinas del rectángulo delimitador del texto. Para el fondo, los dos puntos se basan en todo el lienzo. Este es el código :

public class GradientTextPage : ContentPage
{
    const string TEXT = "GRADIENT";

    public GradientTextPage ()
    {
        Title = "Gradient Text";

        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 gradient for background
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                new SKPoint(info.Width, info.Height),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw background
            canvas.DrawRect(info.Rect, paint);

            // Set TextSize to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Shift textBounds by that amount
            textBounds.Offset(xText, yText);

            // Create gradient for text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(textBounds.Left, textBounds.Top),
                                new SKPoint(textBounds.Right, textBounds.Bottom),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

La propiedad Shader del objeto SKPaint se establece primero para mostrar un degradado a fin de cubrir el fondo. Los puntos de degradado se establecen en las esquinas superior izquierda e inferior derecha del lienzo.

El código establece la propiedad TextSize del objeto SKPaint para que el texto se muestre al 90 % del ancho del lienzo. Los límites de texto se usan para calcular xText y los valores yText para pasar al método DrawText a fin de centrar el texto.

Pero los puntos de degradado de la segunda llamada CreateLinearGradient deben hacer referencia a la esquina superior izquierda e inferior derecha del texto en relación con el lienzo cuando se muestra. Esto se logra cambiando el rectángulo textBounds por los mismos valores xText y yText:

textBounds.Offset(xText, yText);

Ahora se pueden usar las esquinas superior izquierda e inferior derecha del rectángulo para establecer los puntos inicial y final del degradado.

Animación de un degradado

Hay varias maneras de animar un degradado. Un enfoque consiste en animar los puntos inicial y final. La página Animación de degradado mueve los dos puntos alrededor de un círculo centrado en el lienzo. El radio de este círculo es la mitad del ancho o alto del lienzo, lo que sea menor. Los puntos inicial y final son opuestos en este círculo y el degradado va de blanco a negro con un modo de mosaico Mirror:

Animación de degradado

El constructor crea el objeto SKCanvasView. Los métodos OnAppearing y OnDisappearing controlan la lógica de animación:

public class GradientAnimationPage : ContentPage
{
    SKCanvasView canvasView;
    bool isAnimating;
    double angle;
    Stopwatch stopwatch = new Stopwatch();

    public GradientAnimationPage()
    {
        Title = "Gradient Animation";

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

    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 = 3000;
        angle = 2 * Math.PI * (stopwatch.ElapsedMilliseconds % duration) / duration;
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

El método OnTimerTick calcula un valor angle que se anima de 0 a 2π cada 3 segundos.

Esta es una manera de calcular los dos puntos de degradado. Se calcula un valor SKPoint denominado vector para extenderse desde el centro del lienzo hasta un punto en el radio del círculo. La dirección de este vector se basa en los valores de seno y coseno del ángulo. Después, se calculan los dos puntos de degradado opuestos: se calcula un punto restando ese vector del punto central y otro punto se calcula sumando el vector al punto central:

public class GradientAnimationPage : 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())
        {
            SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
            int radius = Math.Min(info.Width, info.Height) / 2;
            SKPoint vector = new SKPoint((float)(radius * Math.Cos(angle)),
                                         (float)(radius * Math.Sin(angle)));

            paint.Shader = SKShader.CreateLinearGradient(
                                center - vector,
                                center + vector,
                                new SKColor[] { SKColors.White, SKColors.Black },
                                null,
                                SKShaderTileMode.Mirror);

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Un enfoque algo distinto requiere menos código. Este enfoque usa el método de sobrecarga SKShader.CreateLinearGradient con una transformación de matriz como último argumento. Este enfoque es la versión del ejemplo:

public class GradientAnimationPage : 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.CreateLinearGradient(
                                new SKPoint(0, 0),
                                info.Width < info.Height ? new SKPoint(info.Width, 0) :
                                                           new SKPoint(0, info.Height),
                                new SKColor[] { SKColors.White, SKColors.Black },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Mirror,
                                SKMatrix.MakeRotation((float)angle, info.Rect.MidX, info.Rect.MidY));

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Si el ancho del lienzo es menor que el alto, los dos puntos de degradado se establecen en (0, 0) y (info.Width, 0). La transformación de rotación pasada como último argumento para que CreateLinearGradient gire con eficacia esos dos puntos alrededor del centro de la pantalla.

Ten en cuenta que si el ángulo es 0, no hay rotación y los dos puntos de degradado son las esquinas superior izquierda y superior derecha del lienzo. Esos puntos no son los mismos puntos de degradado calculados como se muestra en la llamada anterior CreateLinearGradient. Pero estos puntos son paralelos a la línea de degradado horizontal que corta en dos el centro del lienzo y generan un degradado idéntico.

Degradado de arco iris

La página Degradado de arco iris dibuja un arco iris desde la esquina superior izquierda del lienzo hasta la esquina inferior derecha. Pero este degradado de arco iris no es como un arco iris real. Es recto en lugar de curvado, pero se basa en ocho colores HSL (matiz-saturación-luminosidad) que se determinan por ciclos mediante valores de matiz comprendidos entre 0 y 360:

SKColor[] colors = new SKColor[8];

for (int i = 0; i < colors.Length; i++)
{
    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
}

Ese código forma parte del controlador PaintSurface que se muestra a continuación. El controlador comienza creando una ruta de acceso que define un polígono de seis lados que se extiende desde la esquina superior izquierda hasta la esquina inferior derecha del lienzo:

public class RainbowGradientPage : ContentPage
{
    public RainbowGradientPage ()
    {
        Title = "Rainbow Gradient";

        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 (SKPath path = new SKPath())
        {
            float rainbowWidth = Math.Min(info.Width, info.Height) / 2f;

            // Create path from upper-left to lower-right corner
            path.MoveTo(0, 0);
            path.LineTo(rainbowWidth / 2, 0);
            path.LineTo(info.Width, info.Height - rainbowWidth / 2);
            path.LineTo(info.Width, info.Height);
            path.LineTo(info.Width - rainbowWidth / 2, info.Height);
            path.LineTo(0, rainbowWidth / 2);
            path.Close();

            using (SKPaint paint = new SKPaint())
            {
                SKColor[] colors = new SKColor[8];

                for (int i = 0; i < colors.Length; i++)
                {
                    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
                }

                paint.Shader = SKShader.CreateLinearGradient(
                                    new SKPoint(0, rainbowWidth / 2),
                                    new SKPoint(rainbowWidth / 2, 0),
                                    colors,
                                    null,
                                    SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

Los dos puntos de degradado del método CreateLinearGradient se basan en dos de los puntos que definen esta ruta de acceso: ambos puntos están cerca de la esquina superior izquierda. El primero está en el borde superior del lienzo y el segundo en el borde izquierdo del lienzo. Este es el resultado:

Degradado del arco iris defectuoso

Es una imagen interesante, pero no es esa la intención. El problema es que al crear un degradado lineal, las líneas de color constante son perpendiculares a la línea de degradado. La línea de degradado se basa en los puntos en los que la figura toca los lados superior e izquierdo, y esa línea generalmente no es perpendicular a los bordes de la figura que se extienden a la esquina inferior derecha. Este enfoque solo funcionaría si el lienzo fuera cuadrado.

Para crear un degradado de arco iris adecuado, la línea de degradado debe ser perpendicular al borde de arco iris. Es un cálculo más complicado. Debe definirse un vector que sea paralelo al lado largo de la figura. El vector gira 90 grados para que sea perpendicular a ese lado. Después, se alargará para que sea el ancho de la figura; para ello se multiplica por rainbowWidth. Los dos puntos de degradado se calculan en función de un punto en el lado de la figura y ese punto más el vector. Este es el código que se muestra en la página Rainbow Gradient del ejemplo:

public class RainbowGradientPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPath path = new SKPath())
        {
            ···
            using (SKPaint paint = new SKPaint())
            {
                ···
                // Vector on lower-left edge, from top to bottom
                SKPoint edgeVector = new SKPoint(info.Width - rainbowWidth / 2, info.Height) -
                                     new SKPoint(0, rainbowWidth / 2);

                // Rotate 90 degrees counter-clockwise:
                SKPoint gradientVector = new SKPoint(edgeVector.Y, -edgeVector.X);

                // Normalize
                float length = (float)Math.Sqrt(Math.Pow(gradientVector.X, 2) +
                                                Math.Pow(gradientVector.Y, 2));
                gradientVector.X /= length;
                gradientVector.Y /= length;

                // Make it the width of the rainbow
                gradientVector.X *= rainbowWidth;
                gradientVector.Y *= rainbowWidth;

                // Calculate the two points
                SKPoint point1 = new SKPoint(0, rainbowWidth / 2);
                SKPoint point2 = point1 + gradientVector;

                paint.Shader = SKShader.CreateLinearGradient(point1,
                                                             point2,
                                                             colors,
                                                             null,
                                                             SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

Ahora los colores del arco iris están alineados con la figura:

Degradado de arco iris

Colores infinitos

También se usa un degradado de arco iris en la página Colores infinitos. En esta página se dibuja un signo infinito mediante un objeto path descrito en el artículo Tres tipos de curvas Bézier. Después, la imagen se colorea con un degradado de arco iris animado que recorre continuamente la imagen.

El constructor crea el objeto SKPath que describe el signo infinito. Una vez creado el trazado, el constructor también puede obtener los límites rectangulares del trazado. Después, calcula un valor denominado gradientCycleLength. Si un degradado se basa en las esquinas superior izquierda e inferior derecha del rectángulo pathBounds, este valor gradientCycleLength es el ancho horizontal total del patrón de degradado:

public class InfinityColorsPage : ContentPage
{
    ···
    SKCanvasView canvasView;

    // Path information
    SKPath infinityPath;
    SKRect pathBounds;
    float gradientCycleLength;

    // Gradient information
    SKColor[] colors = new SKColor[8];
    ···

    public InfinityColorsPage ()
    {
        Title = "Infinity Colors";

        // Create path for infinity sign
        infinityPath = new SKPath();
        infinityPath.MoveTo(0, 0);                                  // Center
        infinityPath.CubicTo(  50,  -50,   95, -100,  150, -100);   // To top of right loop
        infinityPath.CubicTo( 205, -100,  250,  -55,  250,    0);   // To far right of right loop
        infinityPath.CubicTo( 250,   55,  205,  100,  150,  100);   // To bottom of right loop
        infinityPath.CubicTo(  95,  100,   50,   50,    0,    0);   // Back to center  
        infinityPath.CubicTo( -50,  -50,  -95, -100, -150, -100);   // To top of left loop
        infinityPath.CubicTo(-205, -100, -250,  -55, -250,    0);   // To far left of left loop
        infinityPath.CubicTo(-250,   55, -205,  100, -150,  100);   // To bottom of left loop
        infinityPath.CubicTo( -95,  100, - 50,   50,    0,    0);   // Back to center
        infinityPath.Close();

        // Calculate path information
        pathBounds = infinityPath.Bounds;
        gradientCycleLength = pathBounds.Width +
            pathBounds.Height * pathBounds.Height / pathBounds.Width;

        // Create SKColor array for gradient
        for (int i = 0; i < colors.Length; i++)
        {
            colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
        }

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

El constructor también crea la matriz colors para el arco iris y el objeto SKCanvasView.

Las invalidaciones de los métodos OnAppearing y OnDisappearing realizan la sobrecarga de la animación. El método OnTimerTick anima el campo offset de 0 a gradientCycleLength cada dos segundos:

public class InfinityColorsPage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    float offset;
    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 = 2;     // seconds
        double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
        offset = (float)(gradientCycleLength * progress);
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

Por último, el controlador PaintSurface representa el signo infinito. Dado que el trazado contiene coordenadas negativas y positivas que rodean un punto central de (0, 0), se usa una transformación Translate en el lienzo para desplazarla al centro. La transformación de traslación va seguida de una transformación Scale que aplica un factor de escalado que hace que el signo infinito sea lo más grande posible, a la vez que sigue estando en el 95 % del ancho y alto del lienzo.

Observa que la constante STROKE_WIDTH se agrega al ancho y alto del rectángulo delimitador del trazado. El trazado se ilustrará con una línea de este ancho, por lo que el tamaño del infinito representado aumenta la mitad de ese ancho en los cuatro lados:

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

        canvas.Clear();

        // Set transforms to shift path to center and scale to canvas size
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(0.95f *
            Math.Min(info.Width / (pathBounds.Width + STROKE_WIDTH),
                     info.Height / (pathBounds.Height + STROKE_WIDTH)));

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = STROKE_WIDTH;
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(pathBounds.Left, pathBounds.Top),
                                new SKPoint(pathBounds.Right, pathBounds.Bottom),
                                colors,
                                null,
                                SKShaderTileMode.Repeat,
                                SKMatrix.MakeTranslation(offset, 0));

            canvas.DrawPath(infinityPath, paint);
        }
    }
}

Examina los puntos pasados como los dos primeros argumentos de SKShader.CreateLinearGradient. Esos puntos se basan en el rectángulo delimitador del trazado original. El primer punto es (-250, -100) y el segundo es (250, 100). Internamente en SkiaSharp, esos puntos están sometidos a la transformación actual del lienzo para que se alineen correctamente con el signo infinito mostrado.

Sin el último argumento para CreateLinearGradient, verás un degradado de arco iris que se extiende desde la parte superior izquierda del signo infinito hasta la parte inferior derecha. (En realidad, el degradado se extiende desde la esquina superior izquierda hasta la esquina inferior derecha del rectángulo delimitador. El signo infinito representado es mayor que el rectángulo delimitador en la mitad del valor de STROKE_WIDTH en todos los lados. Dado que el degradado es rojo al principio y al final, y el degradado se crea con SKShaderTileMode.Repeat, la diferencia no es perceptible).

Con ese último argumento para CreateLinearGradient, el patrón de degradado recorre continuamente la imagen:

Colores infinitos

Transparencia y degradados

Los colores que contribuyen a un degradado pueden incorporar transparencia. En lugar de un degradado que se desvanece de un color a otro, el degradado puede desvanecerse de un color a transparente.

Puedes usar esta técnica para obtener algunos efectos interesantes. Uno de los ejemplos clásicos muestra un objeto gráfico con su reflexión:

Degradado de reflexión

El texto que está al revés se colorea con un degradado que va del 50 % transparente en la parte superior a ser totalmente transparente en la parte inferior. Estos niveles de transparencia están asociados a valores alfa de 0x80 y 0.

El controlador PaintSurface de la página Degradado de reflexión escala el tamaño del texto al 90 % del ancho del lienzo. Después, calcula los valores xText y yText para colocar el texto a fin de centrarse horizontalmente, pero sentado en una línea de base correspondiente al centro vertical de la página:

public class ReflectionGradientPage : ContentPage
{
    const string TEXT = "Reflection";

    public ReflectionGradientPage ()
    {
        Title = "Reflection Gradient";

        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())
        {
            // Set text color to blue
            paint.Color = SKColors.Blue;

            // Set text size to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to position text above center
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2;

            // Draw unreflected text
            canvas.DrawText(TEXT, xText, yText, paint);

            // Shift textBounds to match displayed text
            textBounds.Offset(xText, yText);

            // Use those offsets to create a gradient for the reflected text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, textBounds.Top),
                                new SKPoint(0, textBounds.Bottom),
                                new SKColor[] { paint.Color.WithAlpha(0),
                                                paint.Color.WithAlpha(0x80) },
                                null,
                                SKShaderTileMode.Clamp);

            // Scale the canvas to flip upside-down around the vertical center
            canvas.Scale(1, -1, 0, yText);

            // Draw reflected text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

Esos valores xText y yText son los mismos que se usan para mostrar el texto reflejado en la llamada de DrawText en la parte inferior del controlador PaintSurface. Pero justo antes de ese código verás una llamada al método Scale de SKCanvas. Este método Scale se escala horizontalmente en 1 (que no hace nada) o verticalmente en -1, que lo pone todo al revés. El centro de rotación se establece en el punto (0, yText), donde yText es el centro vertical del lienzo, calculado originalmente como info.Height dividido entre 2.

Ten en cuenta que Skia usa el degradado para colorear los objetos gráficos antes de las transformaciones del lienzo. Una vez dibujado el texto sin referencia, se desplaza el rectángulo textBounds para que se corresponda con el texto mostrado:

textBounds.Offset(xText, yText);

La llamada de CreateLinearGradient define un degradado desde la parte superior de ese rectángulo hasta la parte inferior. El degradado va de un azul completamente transparente (paint.Color.WithAlpha(0)) a un azul transparente al 50 % (paint.Color.WithAlpha(0x80)). La transformación del lienzo pone el texto al revés, por lo que el azul transparente al 50 % comienza en la línea de base y se convierte en transparente en la parte superior del texto.