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:
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):
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:
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
:
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:
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
:
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:
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:
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:
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:
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.