O gradiente linear SkiaSharp
A SKPaint
classe define uma Color
propriedade que é usada para traçar linhas ou preencher áreas com uma cor sólida. Como alternativa, você pode traçar linhas ou preencher áreas com gradientes, que são misturas graduais de cores:
O tipo mais básico de gradiente é um gradiente linear . A mistura de cores ocorre em uma linha (chamada de linha de gradiente) de um ponto a outro. As linhas perpendiculares à linha de gradiente têm a mesma cor. Criar um gradiente linear usando um dos dois métodos estáticos SKShader.CreateLinearGradient
. A diferença entre as duas sobrecargas é que uma inclui uma transformação matricial e a outra não.
Esses métodos retornam um objeto do tipo SKShader
que você definiu como a Shader
propriedade de SKPaint
. Se a Shader
propriedade não for nula, ela substituirá a Color
propriedade. Qualquer linha traçada ou qualquer área preenchida usando esse SKPaint
objeto é baseada no gradiente e não na cor sólida.
Observação
A Shader
propriedade é ignorada quando você inclui um SKPaint
objeto em uma DrawBitmap
chamada. Você pode usar a Color
propriedade de para definir um nível de SKPaint
transparência para exibir um bitmap (conforme descrito no artigo Exibindo bitmaps SkiaSharp), mas não pode usar a Shader
propriedade para exibir um bitmap com uma transparência de gradiente. Outras técnicas estão disponíveis para exibir bitmaps com transparências de gradiente: Elas são descritas nos artigos SkiaSharp circulares gradientes e SkiaSharp compositing and blend modes.
Gradientes de canto a canto
Muitas vezes, um gradiente linear se estende de um canto de um retângulo para outro. Se o ponto inicial for o canto superior esquerdo do retângulo, o gradiente poderá se estender:
- verticalmente para o canto inferior esquerdo
- horizontalmente para o canto superior direito
- diagonalmente para o canto inferior direito
O gradiente linear diagonal é demonstrado na primeira página na seção SkiaSharp Shaders and Other Effects da amostra. A página Gradiente de canto a canto cria um SKCanvasView
em seu construtor. O PaintSurface
manipulador cria um SKPaint
objeto em uma using
instrução e, em seguida, define um retângulo quadrado de 300 pixels centralizado na tela:
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 Shader
propriedade de SKPaint
é atribuído o SKShader
valor de retorno do método estático SKShader.CreateLinearGradient
. Os cinco argumentos são os seguintes:
- O ponto inicial do gradiente, definido aqui no canto superior esquerdo do retângulo
- O ponto final do gradiente, definido aqui no canto inferior direito do retângulo
- Uma matriz de duas ou mais cores que contribuem para o gradiente
- Uma matriz de
float
valores indicando a posição relativa das cores dentro da linha de gradiente - Um membro da
SKShaderTileMode
enumeração que indica como o gradiente se comporta além das extremidades da linha de gradiente
Depois que o objeto de gradiente é criado, o DrawRect
método desenha o retângulo quadrado de 300 pixels usando o SKPaint
objeto que inclui o sombreador. Aqui ele está sendo executado no iOS, Android e na Plataforma Universal do Windows (UWP):
A linha de gradiente é definida pelos dois pontos especificados como os dois primeiros argumentos. Observe que esses pontos são relativos à tela e não ao objeto gráfico exibido com o gradiente. Ao longo da linha de gradiente, a cor gradualmente transita de vermelho no canto superior esquerdo para azul no canto inferior direito. Qualquer linha perpendicular à linha de gradiente tem uma cor constante.
A matriz de float
valores especificada como o quarto argumento tem uma correspondência um-para-um com a matriz de cores. Os valores indicam a posição relativa ao longo da linha de gradiente onde essas cores ocorrem. Aqui, o 0 significa que Red
ocorre no início da linha de gradiente e 1 significa que Blue
ocorre no final da linha. Os números devem ser ascendentes, e devem estar no intervalo de 0 a 1. Se eles não estiverem nessa faixa, eles serão ajustados para estar nessa faixa.
Os dois valores na matriz podem ser definidos como algo diferente de 0 e 1. Tente o seguinte:
new float[] { 0.25f, 0.75f }
Agora, todo o primeiro quarto da linha de gradiente é vermelho puro, e o último trimestre é azul puro. A mistura de vermelho e azul é restrita à metade central da linha de gradiente.
Geralmente, convém espaçar esses valores de posição igualmente de 0 a 1. Se esse for o caso, você pode simplesmente fornecer null
como quarto argumento para CreateLinearGradient
.
Embora esse gradiente seja definido entre dois cantos do retângulo quadrado de 300 pixels, ele não se restringe ao preenchimento desse retângulo. A página Gradiente de canto a canto inclui algum código extra que responde a toques ou cliques do mouse na página. O drawBackground
campo é alternado entre true
e false
com cada toque. Se o valor for true
, o PaintSurface
manipulador usará o mesmo SKPaint
objeto para preencher toda a tela e, em seguida, desenhará um retângulo preto indicando o retângulo menor:
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);
}
}
}
}
Veja o que você verá depois de tocar na tela:
Observe que o gradiente se repete no mesmo padrão além dos pontos que definem a linha de gradiente. Essa repetição ocorre porque o último argumento a CreateLinearGradient
ser é SKShaderTileMode.Repeat
. (Você verá as outras opções em breve.)
Observe também que os pontos que você usa para especificar a linha de gradiente não são exclusivos. As linhas perpendiculares à linha de gradiente têm a mesma cor, portanto, há um número infinito de linhas de gradiente que você pode especificar para o mesmo efeito. Por exemplo, ao preencher um retângulo com um gradiente horizontal, você pode especificar os cantos superior esquerdo e superior direito, ou os cantos inferior esquerdo e inferior direito, ou quaisquer dois pontos que estejam pares e paralelos a essas linhas.
Experiência interativa
Você pode experimentar interativamente gradientes lineares com a página Gradiente linear interativo. Esta página usa a classe introduzida InteractivePage
no artigo Três maneiras de desenhar um arco. InteractivePage
manipula TouchEffect
eventos para manter uma coleção de TouchPoint
objetos que você pode mover com os dedos ou o mouse.
O arquivo XAML anexa o TouchEffect
a um pai do SKCanvasView
e também inclui um Picker
que permite selecionar um dos três membros da SKShaderTileMode
enumeração:
<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>
O construtor no arquivo code-behind cria dois TouchPoint
objetos para os pontos inicial e final do gradiente linear. O PaintSurface
manipulador define uma matriz de três cores (para um gradiente de vermelho para verde para azul) e obtém a corrente SKShaderTileMode
do 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);
}
···
}
}
O PaintSurface
manipulador cria o objeto a SKShader
partir de todas essas informações e o usa para colorir toda a tela. A matriz de float
valores é definida como null
. Caso contrário, para espaçar igualmente três cores, você definiria esse parâmetro como uma matriz com os valores 0, 0,5 e 1.
A maior parte do manipulador é dedicada à exibição de vários objetos: os pontos de toque como círculos de contorno, a linha de PaintSurface
gradiente e as linhas perpendiculares às linhas de gradiente nos pontos de toque:
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);
}
}
}
A linha de gradiente que liga os dois pontos de contato é fácil de desenhar, mas as linhas perpendiculares exigem um pouco mais de trabalho. A linha de gradiente é convertida em um vetor, normalizada para ter um comprimento de uma unidade e, em seguida, girada em 90 graus. Esse vetor recebe então um comprimento de 200 pixels. Ele é usado para desenhar quatro linhas que se estendem dos pontos de contato para serem perpendiculares à linha de gradiente.
As linhas perpendiculares coincidem com o início e o fim do gradiente. O que acontece além dessas linhas depende da configuração da SKShaderTileMode
enumeração:
As três capturas de tela mostram os resultados dos três valores diferentes de SKShaderTileMode
. A captura de tela do iOS mostra SKShaderTileMode.Clamp
, que apenas estende as cores na borda do gradiente. A SKShaderTileMode.Repeat
opção na captura de tela do Android mostra como o padrão de gradiente é repetido. A SKShaderTileMode.Mirror
opção na captura de tela UWP também repete o padrão, mas o padrão é invertido a cada vez, resultando em nenhuma descontinuidade de cor.
Gradientes em gradientes
A SKShader
classe não define propriedades ou métodos públicos, exceto para Dispose
. Os SKShader
objetos criados por seus métodos estáticos são, portanto, imutáveis. Mesmo se você usar o mesmo gradiente para dois objetos diferentes, é provável que você queira variar um pouco o gradiente. Para fazer isso, você precisará criar um novo SKShader
objeto.
A página Texto de gradiente exibe texto e um brackground que são coloridos com gradientes semelhantes:
As únicas diferenças nos gradientes são os pontos inicial e final. O gradiente usado para exibir texto é baseado em dois pontos nos cantos do retângulo delimitador do texto. Para o plano de fundo, os dois pontos são baseados em toda a tela. Este é o 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);
}
}
}
A Shader
propriedade do objeto é definida primeiro para exibir um gradiente para cobrir o plano de SKPaint
fundo. Os pontos de gradiente são definidos nos cantos superior esquerdo e inferior direito da tela.
O código define a TextSize
SKPaint
propriedade do objeto para que o texto seja exibido em 90% da largura da tela. Os limites de texto são usados para calcular xText
e yText
os valores a serem passados para o DrawText
método para centralizar o texto.
No entanto, os pontos de gradiente para a segunda CreateLinearGradient
chamada devem se referir ao canto superior esquerdo e inferior direito do texto em relação à tela quando ele é exibido. Isso é feito deslocando o textBounds
retângulo pelos mesmos xText
e yText
valores:
textBounds.Offset(xText, yText);
Agora, os cantos superior esquerdo e inferior direito do retângulo podem ser usados para definir os pontos inicial e final do gradiente.
Animando um gradiente
Há várias maneiras de animar um gradiente. Uma abordagem é animar os pontos de início e fim. A página Animação de gradiente move os dois pontos em um círculo centralizado na tela. O raio desse círculo é metade da largura ou altura da tela, o que for menor. Os pontos inicial e final são opostos um ao outro neste círculo, e o gradiente vai de branco para preto com um Mirror
modo de bloco:
O construtor cria o SKCanvasView
. Os OnAppearing
métodos e OnDisappearing
manipulam a lógica de animação:
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;
}
···
}
O OnTimerTick
método calcula um angle
valor que é animado de 0 a 2π a cada 3 segundos.
Aqui está uma maneira de calcular os dois pontos de gradiente. Um SKPoint
valor nomeado vector
é calculado para se estender do centro da tela até um ponto no raio do círculo. A direção deste vetor é baseada nos valores de seno e cosseno do ângulo. Os dois pontos de gradiente opostos são então calculados: um ponto é calculado subtraindo esse vetor do ponto central, e outro ponto é calculado adicionando o vetor ao ponto 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);
}
}
}
Uma abordagem um pouco diferente requer menos código. Essa abordagem faz uso do método de sobrecarga com uma transformação matricial SKShader.CreateLinearGradient
como último argumento. Essa abordagem é a versão no exemplo:
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);
}
}
}
Se a largura da tela for menor que a altura, os dois pontos de gradiente serão definidos como (0, 0) e (info.Width
, 0). A transformação de rotação passou como o último argumento para CreateLinearGradient
girar efetivamente esses dois pontos ao redor do centro da tela.
Observe que, se o ângulo for 0, não haverá rotação e os dois pontos de gradiente serão os cantos superior esquerdo e superior direito da tela. Esses pontos não são os mesmos pontos de gradiente calculados como mostrado na chamada anterior CreateLinearGradient
. Mas esses pontos são paralelos à linha de gradiente horizontal que corta o centro da tela, e resultam em um gradiente idêntico.
Gradiente do arco-íris
A página Gradiente do arco-íris desenha um arco-íris do canto superior esquerdo da tela para o canto inferior direito. Mas esse gradiente de arco-íris não é como um arco-íris de verdade. É reto em vez de curvo, mas é baseado em oito cores HSL (matiz-saturação-luminosidade) que são determinadas pela ciclagem através de valores de matiz de 0 a 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);
}
Esse código faz parte do PaintSurface
manipulador mostrado abaixo. O manipulador começa criando um caminho que define um polígono de seis lados que se estende do canto superior esquerdo da tela até o canto inferior direito:
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);
}
}
}
}
Os dois pontos de gradiente no método são baseados em CreateLinearGradient
dois dos pontos que definem esse caminho: Ambos os pontos estão próximos ao canto superior esquerdo. O primeiro está na borda superior da tela e o segundo está na borda esquerda da tela. Eis o resultado:
Esta é uma imagem interessante, mas não é bem a intenção. O problema é que, ao criar um gradiente linear, as linhas de cor constante são perpendiculares à linha do gradiente. A linha de gradiente é baseada nos pontos onde a figura toca os lados superior e esquerdo, e essa linha geralmente não é perpendicular às bordas da figura que se estendem até o canto inferior direito. Essa abordagem só funcionaria se a tela fosse quadrada.
Para criar um gradiente arco-íris adequado, a linha de gradiente deve ser perpendicular à borda do arco-íris. Esse é um cálculo mais envolvido. Deve ser definido um vetor paralelo ao lado longo da figura. O vetor é girado 90 graus para que seja perpendicular a esse lado. Ele é então alongado para ser a largura da figura, multiplicando por rainbowWidth
. Os dois pontos de gradiente são calculados com base em um ponto no lado da figura, e esse ponto mais o vetor. Aqui está o código que aparece na página Rainbow Gradient no exemplo:
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);
}
}
}
}
Agora as cores do arco-íris estão alinhadas com a figura:
Infinitas Cores
Um gradiente arco-íris também é usado na página Infinity Colors . Esta página desenha um sinal de infinito usando um objeto de caminho descrito no artigo Três tipos de curvas de Bézier. A imagem é então colorida com um gradiente arco-íris animado que varre continuamente a imagem.
O construtor cria o SKPath
objeto que descreve o sinal de infinito. Depois que o caminho é criado, o construtor também pode obter os limites retangulares do caminho. Em seguida, calcula um valor chamado gradientCycleLength
. Se um gradiente for baseado nos cantos superior esquerdo e inferior direito do pathBounds
retângulo, esse gradientCycleLength
valor será a largura horizontal total do padrão de gradiente:
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;
}
···
}
O construtor também cria a colors
matriz para o arco-íris e o SKCanvasView
objeto.
Substituições dos OnAppearing
métodos e OnDisappearing
executar a sobrecarga para a animação. O OnTimerTick
método anima o offset
campo de 0 a gradientCycleLength
cada dois 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;
}
···
}
Finalmente, o PaintSurface
manipulador renderiza o sinal de infinito. Como o caminho contém coordenadas negativas e positivas ao redor de um ponto central de (0, 0), uma Translate
transformação na tela é usada para deslocá-lo para o centro. A transformação de tradução é seguida por uma Scale
transformação que aplica um fator de escala que torna o sinal de infinito o maior possível, permanecendo dentro de 95% da largura e altura da tela.
Observe que a STROKE_WIDTH
constante é adicionada à largura e altura do retângulo delimitador de caminho. O caminho será traçado com uma linha dessa largura, de modo que o tamanho do infinito renderizado seja aumentado pela metade dessa largura em todos os quatro 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);
}
}
}
Veja os pontos passados como os dois primeiros argumentos do SKShader.CreateLinearGradient
. Esses pontos são baseados no retângulo delimitador do caminho original. O primeiro ponto é (-250, -100) e o segundo é (250, 100). Internos ao SkiaSharp, esses pontos são submetidos à transformação de tela atual para que se alinhem corretamente com o sinal de infinito exibido.
Sem o último argumento para CreateLinearGradient
, você veria um gradiente arco-íris que se estende da parte superior esquerda do sinal do infinito até a parte inferior direita. (Na verdade, o gradiente se estende do canto superior esquerdo até o canto inferior direito do retângulo delimitador. O sinal de infinito renderizado é maior que o retângulo delimitador pela metade do STROKE_WIDTH
valor em todos os lados. Como o gradiente é vermelho no início e no fim, e o gradiente é criado com SKShaderTileMode.Repeat
, a diferença não é perceptível.)
Com esse último argumento para CreateLinearGradient
, o padrão de gradiente varre continuamente a imagem:
Transparência e gradientes
As cores que contribuem para um gradiente podem incorporar transparência. Em vez de um gradiente que desaparece de uma cor para outra, o gradiente pode desaparecer de uma cor para transparente.
Você pode usar esta técnica para alguns efeitos interessantes. Um dos exemplos clássicos mostra um objeto gráfico com seu reflexo:
O texto que está de cabeça para baixo é colorido com um gradiente que é 50% transparente na parte superior para totalmente transparente na parte inferior. Esses níveis de transparência estão associados a valores de alfa de 0x80 e 0.
O PaintSurface
manipulador na página Gradiente de Reflexão dimensiona o tamanho do texto para 90% da largura da tela. Em seguida, calcula xText
e yText
valoriza para posicionar o texto a ser centralizado horizontalmente, mas sentado em uma linha de base correspondente ao centro vertical da 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);
}
}
}
Esses xText
e yText
valores são os mesmos valores usados para exibir o texto refletido DrawText
na chamada na parte inferior do PaintSurface
manipulador. Pouco antes desse código, no entanto, você verá uma chamada para o Scale
método de SKCanvas
. Esse Scale
método escala horizontalmente por 1 (que não faz nada), mas verticalmente por –1, o que efetivamente inverte tudo. O centro de rotação é definido para o ponto (0, yText
), onde yText
está o centro vertical da tela, originalmente calculado como info.Height
dividido por 2.
Lembre-se de que o Skia usa o gradiente para colorir objetos gráficos antes das transformações de tela. Depois que o texto não refletido é desenhado, o retângulo é deslocado textBounds
para que corresponda ao texto exibido:
textBounds.Offset(xText, yText);
A CreateLinearGradient
chamada define um gradiente da parte superior desse retângulo até a parte inferior. O gradiente é de um azul completamente transparente (paint.Color.WithAlpha(0)
) para um azul 50% transparente (paint.Color.WithAlpha(0x80)
). A transformação de tela inverte o texto de cabeça para baixo, de modo que o azul transparente de 50% começa na linha de base e se torna transparente na parte superior do texto.