Efeitos de caminho no SkiaSharp
Descubra os vários efeitos de caminho que permitem que os caminhos sejam usados para acariciar e preencher
Um efeito de caminho é uma instância da SKPathEffect
classe que é criada com um dos oito métodos de criação estáticos definidos pela classe. O SKPathEffect
objeto é então definido como a PathEffect
propriedade de um SKPaint
objeto para uma variedade de efeitos interessantes, por exemplo, acariciando uma linha com um pequeno caminho replicado:
Os efeitos de caminho permitem:
- Traçar uma linha com pontos e traços
- Traçar uma linha com qualquer caminho preenchido
- Preencher uma área com linhas de hachura
- Preencher uma área com um caminho lado a lado
- Faça cantos afiados arredondados
- Adicionar "jitter" aleatório a linhas e curvas
Além disso, você pode combinar dois ou mais efeitos de caminho.
Este artigo também demonstra como usar o GetFillPath
método de SKPaint
converter um caminho em outro caminho aplicando propriedades de SKPaint
, incluindo StrokeWidth
e PathEffect
. Isso resulta em algumas técnicas interessantes, como a obtenção de um caminho que é um esboço de outro caminho. GetFillPath
também é útil em conexão com efeitos de caminho.
Pontos e traços
O uso do PathEffect.CreateDash
método foi descrito no artigo Pontos e Traços. O primeiro argumento do método é uma matriz que contém um número par de dois ou mais valores, alternando entre comprimentos de traços e comprimentos de intervalos entre os traços:
public static SKPathEffect CreateDash (Single[] intervals, Single phase)
Esses valores não são relativos à largura do traçado. Por exemplo, se a largura do traçado for 10 e você quiser uma linha composta de traços quadrados e intervalos quadrados, defina a intervals
matriz como { 10, 10 }. O phase
argumento indica onde dentro do padrão de traço a linha começa. Neste exemplo, se você quiser que a linha comece com a lacuna quadrada, defina phase
como 10.
As extremidades dos traços são afetadas pela StrokeCap
propriedade de SKPaint
. Para larguras de traçado largas, é muito comum definir essa propriedade como SKStrokeCap.Round
arredondar as extremidades dos traços. Nesse caso, os intervals
valores na matriz não incluem o comprimento extra resultante do arredondamento. Este fato significa que um ponto circular requer a especificação de uma largura de zero. Para uma largura de traçado de 10, para criar uma linha com pontos circulares e lacunas entre os pontos do mesmo diâmetro, use uma intervals
matriz de { 0, 20 }.
A página Texto pontilhado animado é semelhante à página Texto Destacado descrita no artigo Integrando texto e elementos gráficos, pois exibe caracteres de texto destacados definindo a Style
SKPaint
propriedade do objeto como SKPaintStyle.Stroke
. Além disso, o Texto Pontilhado Animado usa SKPathEffect.CreateDash
para dar a esse contorno uma aparência pontilhada, e o programa também anima o phase
SKPathEffect.CreateDash
argumento do método para fazer com que os pontos pareçam viajar ao redor dos caracteres de texto. Aqui está a página no modo paisagem:
A AnimatedDottedTextPage
classe começa definindo algumas constantes e também substitui os OnAppearing
métodos e OnDisappearing
para a animação:
public class AnimatedDottedTextPage : ContentPage
{
const string text = "DOTTED";
const float strokeWidth = 10;
static readonly float[] dashArray = { 0, 2 * strokeWidth };
SKCanvasView canvasView;
bool pageIsActive;
public AnimatedDottedTextPage()
{
Title = "Animated Dotted Text";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
protected override void OnAppearing()
{
base.OnAppearing();
pageIsActive = true;
Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
{
canvasView.InvalidateSurface();
return pageIsActive;
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
pageIsActive = false;
}
...
}
O PaintSurface
manipulador começa criando um SKPaint
objeto para exibir o texto. A TextSize
propriedade é ajustada com base na largura da tela:
public class AnimatedDottedTextPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Create an SKPaint object to display the text
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = strokeWidth,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue,
})
{
// Adjust TextSize property so text is 95% of screen width
float textWidth = textPaint.MeasureText(text);
textPaint.TextSize *= 0.95f * info.Width / textWidth;
// Find the text bounds
SKRect textBounds = new SKRect();
textPaint.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;
// Animate the phase; t is 0 to 1 every second
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 1 / 1);
float phase = -t * 2 * strokeWidth;
// Create dotted line effect based on dash array and phase
using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
{
// Set it to the paint object
textPaint.PathEffect = dashEffect;
// And draw the text
canvas.DrawText(text, xText, yText, textPaint);
}
}
}
}
No final do método, o SKPathEffect.CreateDash
método é chamado usando o dashArray
que é definido como um campo e o valor animado phase
. A SKPathEffect
instância é definida como a PathEffect
propriedade do SKPaint
objeto para exibir o texto.
Como alternativa, você pode definir o SKPathEffect
objeto para o SKPaint
objeto antes de medir o texto e centralizá-lo na página. Nesse caso, no entanto, os pontos e traços animados causam alguma variação no tamanho do texto renderizado, e o texto tende a vibrar um pouco. (Experimente!)
Você também notará que, à medida que os pontos animados circulam em torno dos caracteres de texto, há um certo ponto em cada curva fechada onde os pontos parecem aparecer e sair da existência. É aqui que o caminho que define o contorno do caractere começa e termina. Se o comprimento do caminho não for um múltiplo integral do comprimento do padrão de traço (neste caso, 20 pixels), apenas parte desse padrão poderá caber no final do caminho.
É possível ajustar o comprimento do padrão de traço para ajustar o comprimento do caminho, mas isso requer determinar o comprimento do caminho, uma técnica abordada no artigo Informações e enumeração do caminho.
O programa Dot / Dash Morph anima o próprio padrão de traço para que os traços pareçam se dividir em pontos, que se combinam para formar traços novamente:
A DotDashMorphPage
classe substitui os OnAppearing
métodos e OnDisappearing
exatamente como o programa anterior, mas a classe define o SKPaint
objeto como um campo:
public class DotDashMorphPage : ContentPage
{
const float strokeWidth = 30;
static readonly float[] dashArray = new float[4];
SKCanvasView canvasView;
bool pageIsActive = false;
SKPaint ellipsePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = strokeWidth,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Create elliptical path
using (SKPath ellipsePath = new SKPath())
{
ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));
// Create animated path effect
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 3 / 3);
float phase = 0;
if (t < 0.25f) // 1, 0, 1, 2 --> 0, 2, 0, 2
{
float tsub = 4 * t;
dashArray[0] = strokeWidth * (1 - tsub);
dashArray[1] = strokeWidth * 2 * tsub;
dashArray[2] = strokeWidth * (1 - tsub);
dashArray[3] = strokeWidth * 2;
}
else if (t < 0.5f) // 0, 2, 0, 2 --> 1, 2, 1, 0
{
float tsub = 4 * (t - 0.25f);
dashArray[0] = strokeWidth * tsub;
dashArray[1] = strokeWidth * 2;
dashArray[2] = strokeWidth * tsub;
dashArray[3] = strokeWidth * 2 * (1 - tsub);
phase = strokeWidth * tsub;
}
else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
{
float tsub = 4 * (t - 0.5f);
dashArray[0] = strokeWidth * (1 - tsub);
dashArray[1] = strokeWidth * 2;
dashArray[2] = strokeWidth * (1 - tsub);
dashArray[3] = strokeWidth * 2 * tsub;
phase = strokeWidth * (1 - tsub);
}
else // 0, 2, 0, 2 --> 1, 0, 1, 2
{
float tsub = 4 * (t - 0.75f);
dashArray[0] = strokeWidth * tsub;
dashArray[1] = strokeWidth * 2 * (1 - tsub);
dashArray[2] = strokeWidth * tsub;
dashArray[3] = strokeWidth * 2;
}
using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
{
ellipsePaint.PathEffect = pathEffect;
canvas.DrawPath(ellipsePath, ellipsePaint);
}
}
}
}
O PaintSurface
manipulador cria um caminho elíptico com base no tamanho da página e executa uma seção longa de código que define as dashArray
variáveis e phase
. Como a variável t
animada varia de 0 a 1, os if
blocos se dividem nesse tempo em quatro trimestres e, em cada um desses trimestres, tsub
também varia de 0 a 1. No final, o programa cria o SKPathEffect
e o define como objeto SKPaint
para desenho.
De Caminho para Caminho
O GetFillPath
método de SKPaint
transforma um caminho em outro com base nas configurações no SKPaint
objeto. Para ver como isso funciona, substitua a canvas.DrawPath
chamada no programa anterior com o seguinte código:
SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
Nesse novo código, a chamada converte GetFillPath
o ellipsePath
(que é apenas um oval) em newPath
, que é exibido com newPaint
. O newPaint
objeto é criado com todas as configurações de propriedade padrão, exceto que a Style
propriedade é definida com base no valor de retorno booleano de GetFillPath
.
Os visuais são idênticos, exceto pela cor, que é definida ellipsePaint
, mas não newPaint
. Em vez da elipse simples definida no ellipsePath
, newPath
contém numerosos contornos de caminho que definem a série de pontos e traços. Isso é o resultado da aplicação de várias propriedades de ellipsePaint
(especificamente, StrokeWidth
, StrokeCap
e PathEffect
) para ellipsePath
e colocando o caminho resultante em newPath
. O GetFillPath
método retorna um valor booleano indicando se o caminho de destino deve ou não ser preenchido, neste exemplo, o valor de retorno é true
para preencher o caminho.
Tente alterar a Style
configuração para newPaint
SKPaintStyle.Stroke
e você verá os contornos de caminho individuais delineados com uma linha de largura de um pixel.
Acariciando com um caminho
O SKPathEffect.Create1DPath
método é conceitualmente semelhante, SKPathEffect.CreateDash
exceto que você especifica um caminho em vez de um padrão de traços e lacunas. Esse caminho é replicado várias vezes para traçar a linha ou a curva.
A sintaxe do é:
public static SKPathEffect Create1DPath (SKPath path, Single advance,
Single phase, SKPath1DPathEffectStyle style)
Em geral, o caminho para Create1DPath
o qual você passa será pequeno e centrado em torno do ponto (0, 0). O advance
parâmetro indica a distância entre os centros do caminho à medida que o caminho é replicado na linha. Normalmente, você define esse argumento como a largura aproximada do caminho. O phase
argumento desempenha aqui o mesmo papel que desempenha no CreateDash
método.
O SKPath1DPathEffectStyle
tem três membros:
Translate
Rotate
Morph
O Translate
membro faz com que o caminho permaneça na mesma orientação que é replicado ao longo de uma linha ou curva. Para Rotate
, o caminho é girado com base em uma tangente à curva. O caminho tem sua orientação normal para linhas horizontais. Morph
é semelhante a Rotate
exceto que o caminho em si também é curvo para corresponder à curvatura da linha que está sendo traçada.
A página Efeito de Caminho 1D demonstra essas três opções. O arquivo OneDimensionalPathEffectPage.xaml define um seletor que contém três itens correspondentes aos três membros da enumeração:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
Title="1D Path Effect">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker x:Name="effectStylePicker"
Title="Effect Style"
Grid.Row="0"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Translate</x:String>
<x:String>Rotate</x:String>
<x:String>Morph</x:String>
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface"
Grid.Row="1" />
</Grid>
</ContentPage>
O arquivo code-behind OneDimensionalPathEffectPage.xaml.cs define três SKPathEffect
objetos como campos. Todos eles são criados usando SKPathEffect.Create1DPath
objetos SKPath
criados usando SKPath.ParseSvgPathData
o . O primeiro é uma caixa simples, o segundo é uma forma de diamante e o terceiro é um retângulo. Eles são usados para demonstrar os três estilos de efeito:
public partial class OneDimensionalPathEffectPage : ContentPage
{
SKPathEffect translatePathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
24, 0, SKPath1DPathEffectStyle.Translate);
SKPathEffect rotatePathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
20, 0, SKPath1DPathEffectStyle.Rotate);
SKPathEffect morphPathEffect =
SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
55, 0, SKPath1DPathEffectStyle.Morph);
SKPaint pathPaint = new SKPaint
{
Color = SKColors.Blue
};
public OneDimensionalPathEffectPage()
{
InitializeComponent();
}
void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
if (canvasView != null)
{
canvasView.InvalidateSurface();
}
}
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())
{
path.MoveTo(new SKPoint(0, 0));
path.CubicTo(new SKPoint(2 * info.Width, info.Height),
new SKPoint(-info.Width, info.Height),
new SKPoint(info.Width, 0));
switch ((string)effectStylePicker.SelectedItem))
{
case "Translate":
pathPaint.PathEffect = translatePathEffect;
break;
case "Rotate":
pathPaint.PathEffect = rotatePathEffect;
break;
case "Morph":
pathPaint.PathEffect = morphPathEffect;
break;
}
canvas.DrawPath(path, pathPaint);
}
}
}
O PaintSurface
manipulador cria uma curva de Bézier que gira em torno de si mesma e acessa o seletor para determinar qual PathEffect
deve ser usado para traçar o traçado. As três opções — Translate
, Rotate
e Morph
— são mostradas da esquerda para a direita:
O caminho especificado no SKPathEffect.Create1DPath
método é sempre preenchido. O caminho especificado no DrawPath
método será sempre traçado se o SKPaint
objeto tiver sua PathEffect
propriedade definida como um efeito de caminho 1D. Observe que o pathPaint
objeto não tem nenhuma Style
configuração, que normalmente tem Fill
como padrão , mas o caminho é traçado independentemente disso.
A caixa usada no exemplo é quadrada Translate
de 20 pixels e o advance
argumento é definido como 24. Essa diferença causa uma lacuna entre as caixas quando a linha é aproximadamente horizontal ou vertical, mas as caixas se sobrepõem um pouco quando a linha é diagonal porque a diagonal da caixa é de 28,3 pixels.
A forma de diamante no exemplo também tem 20 pixels de Rotate
largura. O advance
é definido como 20 para que os pontos continuem a tocar à medida que o diamante é girado junto com a curvatura da linha.
A forma do retângulo no Morph
exemplo tem 50 pixels de largura com uma advance
configuração de 55 para fazer um pequeno espaço entre os retângulos à medida que eles são dobrados ao redor da curva de Bézier.
Se o advance
argumento for menor que o tamanho do caminho, os caminhos replicados poderão se sobrepor. Isso pode resultar em alguns efeitos interessantes. A página Cadeia Ligada exibe uma série de círculos sobrepostos que parecem se assemelhar a uma cadeia ligada, que fica pendurada na forma distinta de uma catenária:
Olhe muito de perto e verá que esses não são realmente círculos. Cada elo da corrente tem dois arcos, dimensionados e posicionados de modo que pareçam se conectar com elos adjacentes.
Uma corrente ou cabo de distribuição uniforme de peso pende na forma de uma catenária. Um arco construído na forma de uma catenária invertida se beneficia de uma distribuição igual de pressão do peso de um arco. A catenária tem uma descrição matemática aparentemente simples:
y = a · cosh(x / a)
O cosh é a função cosseno hiperbólica. Para x igual a 0, cosh é zero e y é igual a a. Esse é o centro da catenária. Como a função cosseno, cosh é dito ser par, o que significa que cosh(–x) é igual a cosh(x), e os valores aumentam para aumentar argumentos positivos ou negativos. Esses valores descrevem as curvas que formam as laterais da catenária.
Encontrar o valor adequado de a para ajustar a catenária às dimensões da página do telefone não é um cálculo direto. Se w e h são a largura e a altura de um retângulo, o valor ótimo de a satisfaz a seguinte equação:
cosh(w / 2 / a) = 1 + h / a
O método a seguir na classe incorpora LinkedChainPage
essa igualdade referindo-se às duas expressões à esquerda e à direita do sinal de igual como left
e right
. Para valores pequenos de a, left
é maior que right
; para valores grandes de a, left
é menor que right
. O while
loop se estreita em um valor ótimo de um:
float FindOptimumA(float width, float height)
{
Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
Func<float, float> right = (float a) => 1 + height / a;
float gtA = 1; // starting value for left > right
float ltA = 10000; // starting value for left < right
while (Math.Abs(gtA - ltA) > 0.1f)
{
float avgA = (gtA + ltA) / 2;
if (left(avgA) < right(avgA))
{
ltA = avgA;
}
else
{
gtA = avgA;
}
}
return (gtA + ltA) / 2;
}
O SKPath
objeto para os links é criado no construtor da classe e o objeto resultante SKPathEffect
é definido como a PathEffect
propriedade do SKPaint
objeto armazenado como um campo:
public class LinkedChainPage : ContentPage
{
const float linkRadius = 30;
const float linkThickness = 5;
Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));
SKPaint linksPaint = new SKPaint
{
Color = SKColors.Silver
};
public LinkedChainPage()
{
Title = "Linked Chain";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Create the path for the individual links
SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
SKRect inner = outer;
inner.Inflate(-linkThickness, -linkThickness);
using (SKPath linkPath = new SKPath())
{
linkPath.AddArc(outer, 55, 160);
linkPath.ArcTo(inner, 215, -160, false);
linkPath.Close();
linkPath.AddArc(outer, 235, 160);
linkPath.ArcTo(inner, 395, -160, false);
linkPath.Close();
// Set that path as the 1D path effect for linksPaint
linksPaint.PathEffect =
SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
SKPath1DPathEffectStyle.Rotate);
}
}
...
}
O principal trabalho do PaintSurface
manipulador é criar um caminho para a própria catenária. Depois de determinar o a ótimo e armazená-lo na optA
variável, ele também precisa calcular um deslocamento a partir do topo da janela. Em seguida, ele pode acumular uma coleção de SKPoint
valores para a catenária, transformá-la em um caminho e desenhar o caminho com o objeto criado SKPaint
anteriormente:
public class LinkedChainPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
// Width and height of catenary
int width = info.Width;
float height = info.Height - linkRadius;
// Find the optimum 'a' for this width and height
float optA = FindOptimumA(width, height);
// Calculate the vertical offset for that value of 'a'
float yOffset = catenary(optA, -width / 2);
// Create a path for the catenary
SKPoint[] points = new SKPoint[width];
for (int x = 0; x < width; x++)
{
points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
}
using (SKPath path = new SKPath())
{
path.AddPoly(points, false);
// And render that path with the linksPaint object
canvas.DrawPath(path, linksPaint);
}
}
...
}
Este programa define o caminho usado em Create1DPath
para ter seu ponto (0, 0) no centro. Isso parece razoável porque o ponto (0, 0) do caminho está alinhado com a linha ou curva que ele está adornando. No entanto, você pode usar um ponto não centralizado (0, 0) para alguns efeitos especiais.
A página Correia transportadora cria um caminho semelhante a uma correia transportadora oblonga com uma parte superior e inferior curvas que são dimensionadas de acordo com as dimensões da janela. Esse caminho é traçado com um objeto simples SKPaint
de 20 pixels de largura e cor cinza e, em seguida, traçado novamente com outro SKPaint
objeto com um SKPathEffect
objeto fazendo referência a um caminho semelhante a um pequeno balde:
O ponto (0, 0) do caminho da caçamba é a alça, então quando o phase
argumento é animado, as caçambas parecem girar em torno da correia transportadora, talvez pegando água na parte inferior e despejando-a na parte superior.
A ConveyorBeltPage
classe implementa animação com substituições dos OnAppearing
métodos e OnDisappearing
. O caminho para o bucket é definido no construtor da página:
public class ConveyorBeltPage : ContentPage
{
SKCanvasView canvasView;
bool pageIsActive = false;
SKPaint conveyerPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 20,
Color = SKColors.DarkGray
};
SKPath bucketPath = new SKPath();
SKPaint bucketsPaint = new SKPaint
{
Color = SKColors.BurlyWood,
};
public ConveyorBeltPage()
{
Title = "Conveyor Belt";
canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Create the path for the bucket starting with the handle
bucketPath.AddRect(new SKRect(-5, -3, 25, 3));
// Sides
bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
SKPathDirection.CounterClockwise);
bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
SKPathDirection.CounterClockwise);
// Five slats
for (int i = 0; i < 5; i++)
{
bucketPath.MoveTo(25, -19 + 8 * i);
bucketPath.LineTo(25, -13 + 8 * i);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
bucketPath.LineTo(65, -19 + 8 * i);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.Clockwise, 25, -19 + 8 * i);
bucketPath.Close();
}
// Arc to suggest the hidden side
bucketPath.MoveTo(25, -17);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.Clockwise, 65, -17);
bucketPath.LineTo(65, -19);
bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
SKPathDirection.CounterClockwise, 25, -19);
bucketPath.Close();
// Make it a little bigger and correct the orientation
bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
}
...
O código de criação do bucket é concluído com duas transformações que tornam o bucket um pouco maior e o tornam lateral. Aplicar essas transformações foi mais fácil do que ajustar todas as coordenadas no código anterior.
O PaintSurface
manipulador começa definindo um caminho para a própria correia transportadora. Este é simplesmente um par de linhas e um par de semicírculos que são desenhados com uma linha cinza-escura de 20 pixels de largura:
public class ConveyorBeltPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float width = info.Width / 3;
float verticalMargin = width / 2 + 150;
using (SKPath conveyerPath = new SKPath())
{
// Straight verticals capped by semicircles on top and bottom
conveyerPath.MoveTo(width, verticalMargin);
conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
SKPathDirection.Clockwise, 2 * width, verticalMargin);
conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
SKPathDirection.Clockwise, width, info.Height - verticalMargin);
conveyerPath.Close();
// Draw the conveyor belt itself
canvas.DrawPath(conveyerPath, conveyerPaint);
// Calculate spacing based on length of conveyer path
float length = 2 * (info.Height - 2 * verticalMargin) +
2 * ((float)Math.PI * width / 2);
// Value will be somewhere around 200
float spacing = length / (float)Math.Round(length / 200);
// Now animate the phase; t is 0 to 1 every 2 seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 2 / 2);
float phase = -t * spacing;
// Create the buckets PathEffect
using (SKPathEffect bucketsPathEffect =
SKPathEffect.Create1DPath(bucketPath, spacing, phase,
SKPath1DPathEffectStyle.Rotate))
{
// Set it to the Paint object and draw the path again
bucketsPaint.PathEffect = bucketsPathEffect;
canvas.DrawPath(conveyerPath, bucketsPaint);
}
}
}
}
A lógica para desenhar a correia transportadora não funciona no modo paisagem.
As caçambas devem ser espaçadas cerca de 200 pixels na correia transportadora. No entanto, a correia transportadora provavelmente não tem um múltiplo de 200 pixels de comprimento, o que significa que, à medida que o phase
argumento de SKPathEffect.Create1DPath
é animado, os baldes aparecerão e sairão da existência.
Por esta razão, o programa primeiro calcula um valor chamado length
que é o comprimento da correia transportadora. Como a correia transportadora é composta por linhas retas e semicírculos, esse é um cálculo simples. Em seguida, o número de baldes é calculado dividindo length
por 200. Isso é arredondado para o número inteiro mais próximo, e esse número é então dividido em length
. O resultado é um espaçamento para um número integral de caçambas. O phase
argumento é simplesmente uma fração disso.
De caminho em caminho novamente
Na parte inferior do DrawSurface
manipulador em Correia transportadora, comente a canvas.DrawPath
chamada e substitua-a pelo seguinte código:
SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);
Como no exemplo anterior do GetFillPath
, você verá que os resultados são os mesmos, exceto pela cor. Após a execução GetFillPath
do , o newPath
objeto contém várias cópias do caminho do bucket, cada uma posicionada no mesmo local em que a animação as posicionou no momento da chamada.
Eclodindo uma área
O SKPathEffect.Create2DLines
método preenche uma área com linhas paralelas, muitas vezes chamadas de linhas de hachura. O método tem a seguinte sintaxe:
public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)
O width
argumento especifica a largura do traçado das linhas de hachura. O matrix
parâmetro é uma combinação de dimensionamento e rotação opcional. O fator de dimensionamento indica o incremento de pixels que o Skia usa para espaçar as linhas de hachura. A separação entre as linhas é o fator de escala menos o width
argumento. Se o fator de escala for menor ou igual ao width
valor, não haverá espaço entre as linhas de hachura e a área parecerá estar preenchida. Especifique o mesmo valor para dimensionamento horizontal e vertical.
Por padrão, as linhas de hachura são horizontais. Se o matrix
parâmetro contiver rotação, as linhas de hachura serão giradas no sentido horário.
A página Preenchimento de hachura demonstra esse efeito de caminho. A HatchFillPage
classe define três efeitos de caminho como campos, o primeiro para linhas de hachura horizontais com uma largura de 3 pixels com um fator de dimensionamento indicando que eles estão espaçados 6 pixels de distância. A separação entre as linhas é, portanto, de três pixels. O segundo efeito de caminho é para linhas de hachura verticais com uma largura de seis pixels espaçadas 24 pixels de distância (portanto, a separação é de 18 pixels), e o terceiro é para linhas de hachura diagonais de 12 pixels de largura espaçadas 36 pixels de distância.
public class HatchFillPage : ContentPage
{
SKPaint fillPaint = new SKPaint();
SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));
SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));
SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
...
static SKMatrix Multiply(SKMatrix first, SKMatrix second)
{
SKMatrix target = SKMatrix.MakeIdentity();
SKMatrix.Concat(ref target, first, second);
return target;
}
}
Observe o método matricial Multiply
. Como os fatores de dimensionamento horizontal e vertical são os mesmos, a ordem em que as matrizes de escala e rotação são multiplicadas não importa.
O PaintSurface
manipulador usa esses três efeitos de caminho com três cores diferentes em combinação com fillPaint
para preencher um retângulo arredondado dimensionado para caber na página. A Style
propriedade definida em é ignorada, quando o SKPaint
objeto inclui um efeito de caminho criado a fillPaint
partir do SKPathEffect.Create2DLine
, a área é preenchida independentemente de:
public class HatchFillPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath roundRectPath = new SKPath())
{
// Create a path
roundRectPath.AddRoundedRect(
new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);
// Horizontal hatch marks
fillPaint.PathEffect = horzLinesPath;
fillPaint.Color = SKColors.Red;
canvas.DrawPath(roundRectPath, fillPaint);
// Vertical hatch marks
fillPaint.PathEffect = vertLinesPath;
fillPaint.Color = SKColors.Blue;
canvas.DrawPath(roundRectPath, fillPaint);
// Diagonal hatch marks -- use clipping
fillPaint.PathEffect = diagLinesPath;
fillPaint.Color = SKColors.Green;
canvas.Save();
canvas.ClipPath(roundRectPath);
canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
canvas.Restore();
// Outline the path
canvas.DrawPath(roundRectPath, strokePaint);
}
}
...
}
Se você olhar com atenção para os resultados, verá que as linhas de escotilha vermelha e azul não estão confinadas precisamente ao retângulo arredondado. (Esta é aparentemente uma característica do código Skia subjacente.) Se isso não for satisfatório, uma abordagem alternativa é mostrada para as linhas de hachura diagonais em verde: o retângulo arredondado é usado como um caminho de recorte e as linhas de hachura são desenhadas em toda a página.
O PaintSurface
manipulador conclui com uma chamada para simplesmente traçar o retângulo arredondado, para que você possa ver a discrepância com as linhas de hachura vermelha e azul:
A tela do Android realmente não se parece com isso: o dimensionamento da captura de tela fez com que as linhas vermelhas finas e os espaços finos se consolidassem em linhas vermelhas aparentemente mais largas e espaços mais amplos.
Preenchendo com um caminho
O SKPathEffect.Create2DPath
permite que você preencha uma área com um caminho que é replicado horizontal e verticalmente, na verdade lado a lado a área:
public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)
Os SKMatrix
fatores de escala indicam o espaçamento horizontal e vertical do caminho replicado. Mas você não pode girar o caminho usando esse matrix
argumento, se quiser que o caminho seja girado, gire o próprio caminho usando o Transform
método definido por SKPath
.
O caminho replicado normalmente é alinhado com as bordas esquerda e superior da tela, em vez da área que está sendo preenchida. Você pode substituir esse comportamento fornecendo fatores de conversão entre 0 e os fatores de dimensionamento para especificar deslocamentos horizontais e verticais dos lados esquerdo e superior.
A página Preenchimento de Bloco de Caminho demonstra esse efeito de caminho. O caminho usado para colocar lado a lado a área é definido como um campo na PathTileFillPage
classe. As coordenadas horizontais e verticais variam de –40 a 40, o que significa que esse caminho tem 80 pixels quadrados:
public class PathTileFillPage : ContentPage
{
SKPath tilePath = SKPath.ParseSvgPathData(
"M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
"40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
"-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
...
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.Color = SKColors.Red;
using (SKPathEffect pathEffect =
SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
{
paint.PathEffect = pathEffect;
canvas.DrawRoundRect(
new SKRect(50, 50, info.Width - 50, info.Height - 50),
100, 100, paint);
}
}
}
}
No manipulador, as PaintSurface
SKPathEffect.Create2DPath
chamadas definem o espaçamento horizontal e vertical como 64 para fazer com que os blocos quadrados de 80 pixels se sobreponham. Felizmente, o caminho se assemelha a uma peça de quebra-cabeça, mesclando bem com azulejos adjacentes:
O dimensionamento da captura de tela original causa alguma distorção, particularmente na tela do Android.
Observe que esses blocos sempre aparecem inteiros e nunca são truncados. Nas duas primeiras capturas de tela, nem é evidente que a área que está sendo preenchida seja um retângulo arredondado. Se você quiser truncar esses blocos para uma área específica, use um caminho de recorte.
Tente definir a Style
SKPaint
propriedade do objeto como Stroke
, e você verá os blocos individuais delineados em vez de preenchidos.
Também é possível preencher uma área com um bitmap lado a lado, como mostrado no artigo SkiaSharp bitmap tiling.
Arredondamento de cantos afiados
O programa Rounded Heptagon apresentado no artigo Three Ways to Draw an Arc usou um arco tangente para curvar os pontos de uma figura de sete lados. A página Outro Heptagon Arredondado mostra uma abordagem muito mais fácil que usa um efeito de caminho criado a partir do SKPathEffect.CreateCorner
método:
public static SKPathEffect CreateCorner (Single radius)
Embora o único argumento seja nomeado radius
, você deve defini-lo para metade do raio de canto desejado. (Esta é uma característica do código Skia subjacente.)
Aqui está o PaintSurface
manipulador na AnotherRoundedHeptagonPage
classe:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int numVertices = 7;
float radius = 0.45f * Math.Min(info.Width, info.Height);
SKPoint[] vertices = new SKPoint[numVertices];
double vertexAngle = -0.5f * Math.PI; // straight up
// Coordinates of the vertices of the polygon
for (int vertex = 0; vertex < numVertices; vertex++)
{
vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
radius * (float)Math.Sin(vertexAngle));
vertexAngle += 2 * Math.PI / numVertices;
}
float cornerRadius = 100;
// Create the path
using (SKPath path = new SKPath())
{
path.AddPoly(vertices, true);
// Render the path in the center of the screen
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Blue;
paint.StrokeWidth = 10;
// Set argument to half the desired corner radius!
paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawPath(path, paint);
// Uncomment DrawCircle call to verify corner radius
float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
paint.Color = SKColors.Green;
// canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
}
}
}
Você pode usar esse efeito com acariciamento ou preenchimento com base na Style
propriedade do SKPaint
objeto. Aqui está em execução:
Você verá que esse heptagon arredondado é idêntico ao programa anterior. Se você precisar de mais convencimento de que o raio de canto é realmente 100 em vez dos 50 especificados na SKPathEffect.CreateCorner
chamada, você pode descomentar a instrução final no programa e ver um círculo de 100 raios sobreposto no canto.
Jitter aleatório
Às vezes, as linhas retas impecáveis da computação gráfica não são exatamente o que você quer, e um pouco de aleatoriedade é desejada. Nesse caso, você vai querer tentar o SKPathEffect.CreateDiscrete
método:
public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)
Você pode usar esse efeito de caminho para acariciar ou preencher. As linhas são separadas em segmentos conectados — cujo comprimento aproximado é especificado por segLength
— e se estendem em direções diferentes. A extensão do desvio em relação à linha original é especificada por deviation
.
O argumento final é uma semente usada para gerar a sequência pseudoaleatória usada para o efeito. O efeito jitter será um pouco diferente para sementes diferentes. O argumento tem um valor padrão de zero, o que significa que o efeito é o mesmo sempre que você executa o programa. Se você quiser um jitter diferente sempre que a tela for pintada novamente, poderá definir a semente para a Millisecond
propriedade de um DataTime.Now
valor (por exemplo).
A página Jitter Experiment permite que você experimente valores diferentes ao acariciar um retângulo:
O programa é simples. O arquivo JitterExperimentPage.xaml instancia dois Slider
elementos e um SKCanvasView
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
Title="Jitter Experiment">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.Resources>
<ResourceDictionary>
<Style TargetType="Label">
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Slider">
<Setter Property="Margin" Value="20, 0" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Maximum" Value="100" />
</Style>
</ResourceDictionary>
</Grid.Resources>
<Slider x:Name="segLengthSlider"
Grid.Row="0"
ValueChanged="sliderValueChanged" />
<Label Text="{Binding Source={x:Reference segLengthSlider},
Path=Value,
StringFormat='Segment Length = {0:F0}'}"
Grid.Row="1" />
<Slider x:Name="deviationSlider"
Grid.Row="2"
ValueChanged="sliderValueChanged" />
<Label Text="{Binding Source={x:Reference deviationSlider},
Path=Value,
StringFormat='Deviation = {0:F0}'}"
Grid.Row="3" />
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="4"
PaintSurface="OnCanvasViewPaintSurface" />
</Grid>
</ContentPage>
O PaintSurface
manipulador no arquivo code-behind JitterExperimentPage.xaml.cs é chamado sempre que um Slider
valor é alterado. Ele chama SKPathEffect.CreateDiscrete
usando os dois Slider
valores e usa isso para traçar um retângulo:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float segLength = (float)segLengthSlider.Value;
float deviation = (float)deviationSlider.Value;
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 5;
paint.Color = SKColors.Blue;
using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
{
paint.PathEffect = pathEffect;
SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
canvas.DrawRect(rect, paint);
}
}
}
Você também pode usar esse efeito para preenchimento, caso em que o contorno da área preenchida está sujeito a esses desvios aleatórios. A página Jitter Text demonstra o uso desse efeito de caminho para exibir texto. A maior parte do código no PaintSurface
manipulador da JitterTextPage
classe é dedicada ao dimensionamento e centralização do texto:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
string text = "FUZZY";
using (SKPaint textPaint = new SKPaint())
{
textPaint.Color = SKColors.Purple;
textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);
// Adjust TextSize property so text is 95% of screen width
float textWidth = textPaint.MeasureText(text);
textPaint.TextSize *= 0.95f * info.Width / textWidth;
// Find the text bounds
SKRect textBounds = new SKRect();
textPaint.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;
canvas.DrawText(text, xText, yText, textPaint);
}
}
Aqui ele está sendo executado no modo paisagem:
Estrutura de tópicos do caminho
Você já viu dois pequenos exemplos do GetFillPath
método do SKPaint
, que existe em duas versões:
public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)
public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)
Apenas os dois primeiros argumentos são necessários. O método acessa o caminho referenciado src
pelo argumento, modifica os dados do caminho com base nas propriedades de traçado no objeto (incluindo a PathEffect
propriedade) e, em SKPaint
seguida, grava os resultados no dst
caminho. O resScale
parâmetro permite reduzir a precisão para criar um caminho de destino menor, e o argumento pode eliminar contornos cullRect
fora de um retângulo.
Um uso básico desse método não envolve efeitos de caminho: se o SKPaint
objeto tiver sua Style
propriedade definida como SKPaintStyle.Stroke
, e não tiver seu PathEffect
conjunto, criará GetFillPath
um caminho que represente um contorno do caminho de origem como se ele tivesse sido traçado pelas propriedades de pintura.
Por exemplo, se o src
caminho for um círculo simples de raio 500 e o SKPaint
objeto especificar uma largura de traçado de 100, o dst
caminho se tornará dois círculos concêntricos, um com um raio de 450 e outro com um raio de 550. O método é chamado GetFillPath
porque preencher esse dst
caminho é o mesmo que acariciar o src
caminho. Mas você também pode traçar o dst
caminho para ver os contornos do caminho.
O Toque para Esboçar o Caminho demonstra isso. Os SKCanvasView
e TapGestureRecognizer
são instanciados no arquivo TapToOutlineThePathPage.xaml . O arquivo code-behind TapToOutlineThePathPage.xaml.cs define três SKPaint
objetos como campos, dois para acariciar com larguras de traçado de 100 e 20 e o terceiro para preenchimento:
public partial class TapToOutlineThePathPage : ContentPage
{
bool outlineThePath = false;
SKPaint redThickStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 100
};
SKPaint redThinStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 20
};
SKPaint blueFill = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue
};
public TapToOutlineThePathPage()
{
InitializeComponent();
}
void OnCanvasViewTapped(object sender, EventArgs args)
{
outlineThePath ^= true;
(sender as SKCanvasView).InvalidateSurface();
}
...
}
Se a tela não tiver sido tocada, o PaintSurface
manipulador usará os blueFill
objetos e redThickStroke
paint para renderizar um caminho circular:
public partial class TapToOutlineThePathPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath circlePath = new SKPath())
{
circlePath.AddCircle(info.Width / 2, info.Height / 2,
Math.Min(info.Width / 2, info.Height / 2) -
redThickStroke.StrokeWidth);
if (!outlineThePath)
{
canvas.DrawPath(circlePath, blueFill);
canvas.DrawPath(circlePath, redThickStroke);
}
else
{
using (SKPath outlinePath = new SKPath())
{
redThickStroke.GetFillPath(circlePath, outlinePath);
canvas.DrawPath(outlinePath, blueFill);
canvas.DrawPath(outlinePath, redThinStroke);
}
}
}
}
}
O círculo é preenchido e traçado como seria de esperar:
Quando você toca na tela, outlineThePath
é definido como true
, e o PaintSurface
manipulador cria um objeto novo SKPath
e o usa como o caminho de destino em uma chamada para GetFillPath
no redThickStroke
objeto paint. Esse caminho de destino é então preenchido e traçado com redThinStroke
, resultando no seguinte:
Os dois círculos vermelhos indicam claramente que o caminho circular original foi convertido em dois contornos circulares.
Esse método pode ser muito útil no desenvolvimento de caminhos a serem usados para o SKPathEffect.Create1DPath
método. Os caminhos especificados nesses métodos são sempre preenchidos quando os caminhos são replicados. Se você não quiser que todo o caminho seja preenchido, você deve definir cuidadosamente os contornos.
Por exemplo, na amostra de Cadeia Encadeada , os elos foram definidos com uma série de quatro arcos, cada par dos quais foram baseados em dois raios para delinear a área do caminho a ser preenchido. É possível substituir o código na LinkedChainPage
classe para fazê-lo um pouco diferente.
Primeiro, convém redefinir a linkRadius
constante:
const float linkRadius = 27.5f;
const float linkThickness = 5;
Agora linkPath
são apenas dois arcos baseados nesse único raio, com os ângulos de início e ângulos de varredura desejados:
using (SKPath linkPath = new SKPath())
{
SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
linkPath.AddArc(rect, 55, 160);
linkPath.AddArc(rect, 235, 160);
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Style = SKPaintStyle.Stroke;
strokePaint.StrokeWidth = linkThickness;
using (SKPath outlinePath = new SKPath())
{
strokePaint.GetFillPath(linkPath, outlinePath);
// Set that path as the 1D path effect for linksPaint
linksPaint.PathEffect =
SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
SKPath1DPathEffectStyle.Rotate);
}
}
}
O outlinePath
objeto é, então, o destinatário da estrutura de tópicos de quando ele é traçado linkPath
com as propriedades especificadas em strokePaint
.
Outro exemplo usando essa técnica está chegando em seguida para o caminho usado em um método.
Combinando efeitos de caminho
Os dois métodos finais de criação estática de SKPathEffect
são SKPathEffect.CreateSum
e SKPathEffect.CreateCompose
:
public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)
public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)
Ambos os métodos combinam dois efeitos de caminho para criar um efeito de caminho composto. O CreateSum
método cria um efeito de caminho que é semelhante aos dois efeitos de caminho aplicados separadamente, enquanto CreateCompose
aplica um efeito de caminho (o inner
) e, em seguida, aplica o outer
a isso.
Você já viu como o GetFillPath
método de pode converter um caminho em outro caminho com base em SKPaint
propriedades (incluindo PathEffect
), portanto, não deve ser muito misterioso como um SKPaint
objeto pode executar essa operação duas vezes com os dois efeitos de SKPaint
caminho especificados nos CreateSum
métodos orCreateCompose
.
Um uso óbvio de é definir um SKPaint
objeto que preenche um caminho com um efeito de caminho e traça o caminho com outro efeito de CreateSum
caminho. Isso é demonstrado no exemplo Cats in Frame , que exibe uma matriz de gatos dentro de um quadro com bordas recortadas:
A CatsInFramePage
classe começa definindo vários campos. Você pode reconhecer o PathDataCatPage
primeiro campo da classe do artigo Dados do caminho SVG. O segundo caminho é baseado em uma linha e arco para o padrão de vieira do quadro:
public class CatsInFramePage : ContentPage
{
// From PathDataCatPage.cs
SKPath catPath = SKPath.ParseSvgPathData(
"M 160 140 L 150 50 220 103" + // Left ear
"M 320 140 L 330 50 260 103" + // Right ear
"M 215 230 L 40 200" + // Left whiskers
"M 215 240 L 40 240" +
"M 215 250 L 40 280" +
"M 265 230 L 440 200" + // Right whiskers
"M 265 240 L 440 240" +
"M 265 250 L 440 280" +
"M 240 100" + // Head
"A 100 100 0 0 1 240 300" +
"A 100 100 0 0 1 240 100 Z" +
"M 180 170" + // Left eye
"A 40 40 0 0 1 220 170" +
"A 40 40 0 0 1 180 170 Z" +
"M 300 170" + // Right eye
"A 40 40 0 0 1 260 170" +
"A 40 40 0 0 1 300 170 Z");
SKPaint catStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 5
};
SKPath scallopPath =
SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");
SKPaint framePaint = new SKPaint
{
Color = SKColors.Black
};
...
}
O catPath
pode ser usado no SKPathEffect.Create2DPath
método se a propriedade object Style
SKPaint
estiver definida como Stroke
. No entanto, se o catPath
for usado diretamente neste programa, toda a cabeça do gato será preenchida, e os bigodes nem serão visíveis. (Experimente!) É necessário obter o esboço desse caminho e usar esse esboço no SKPathEffect.Create2DPath
método.
O construtor faz esse trabalho. Primeiro, ele aplica duas transformações para catPath
mover o ponto (0, 0) para o centro e reduzi-lo em tamanho. GetFillPath
obtém todos os contornos dos contornos em outlinedCatPath
, e esse objeto é usado na SKPathEffect.Create2DPath
chamada. Os fatores de escala no SKMatrix
valor são ligeiramente maiores do que o tamanho horizontal e vertical do gato para fornecer um pequeno buffer entre os blocos, enquanto os fatores de tradução foram derivados um pouco empiricamente para que um gato completo seja visível no canto superior esquerdo do quadro:
public class CatsInFramePage : ContentPage
{
...
public CatsInFramePage()
{
Title = "Cats in Frame";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
// Move (0, 0) point to center of cat path
catPath.Transform(SKMatrix.MakeTranslation(-240, -175));
// Now catPath is 400 by 250
// Scale it down to 160 by 100
catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));
// Get the outlines of the contours of the cat path
SKPath outlinedCatPath = new SKPath();
catStroke.GetFillPath(catPath, outlinedCatPath);
// Create a 2D path effect from those outlines
SKPathEffect fillEffect = SKPathEffect.Create2DPath(
new SKMatrix { ScaleX = 170, ScaleY = 110,
TransX = 75, TransY = 80,
Persp2 = 1 },
outlinedCatPath);
// Create a 1D path effect from the scallop path
SKPathEffect strokeEffect =
SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);
// Set the sum the effects to frame paint
framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
}
...
}
O construtor então chama SKPathEffect.Create1DPath
o quadro escalopado. Observe que a largura do caminho é de 100 pixels, mas o avanço é de 75 pixels para que o caminho replicado seja sobreposto ao redor do quadro. A instrução final do construtor chama SKPathEffect.CreateSum
para combinar os dois efeitos de caminho e definir o resultado para o SKPaint
objeto.
Todo esse trabalho permite que o PaintSurface
manipulador seja bastante simples. Ele só precisa definir um retângulo e desenhá-lo usando framePaint
:
public class CatsInFramePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
canvas.ClipRect(rect);
canvas.DrawRect(rect, framePaint);
}
}
Os algoritmos por trás dos efeitos de caminho sempre fazem com que todo o caminho usado para acariciar ou preencher seja exibido, o que pode fazer com que alguns visuais apareçam fora do retângulo. A ClipRect
chamada antes da DrawRect
chamada permite que o visual seja consideravelmente mais limpo. (Experimente sem recorte!)
É comum usar SKPathEffect.CreateCompose
para adicionar algum jitter a outro efeito de caminho. Você certamente pode experimentar por conta própria, mas aqui está um exemplo um pouco diferente:
As linhas de escotilha tracejadas preenchem uma elipse com linhas de escotilha tracejadas. A maior parte do trabalho na DashedHatchLinesPage
aula é realizada diretamente nas definições de campo. Esses campos definem um efeito de traço e um efeito de hachura. Eles são definidos como static
porque são então referenciados em uma SKPathEffect.CreateCompose
chamada na SKPaint
definição:
public class DashedHatchLinesPage : ContentPage
{
static SKPathEffect dashEffect =
SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);
static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
Multiply(SKMatrix.MakeScale(60, 60),
SKMatrix.MakeRotationDegrees(45)));
SKPaint paint = new SKPaint()
{
PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
StrokeCap = SKStrokeCap.Round,
Color = SKColors.Blue
};
...
static SKMatrix Multiply(SKMatrix first, SKMatrix second)
{
SKMatrix target = SKMatrix.MakeIdentity();
SKMatrix.Concat(ref target, first, second);
return target;
}
}
O PaintSurface
manipulador precisa conter apenas a sobrecarga padrão mais uma chamada para DrawOval
:
public class DashedHatchLinesPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawOval(info.Width / 2, info.Height / 2,
0.45f * info.Width, 0.45f * info.Height,
paint);
}
...
}
Como você já descobriu, as linhas de escotilha não se restringem exatamente ao interior da área e, neste exemplo, elas sempre começam à esquerda com um traço inteiro:
Agora que você já viu efeitos de caminho que variam de simples pontos e traços a combinações estranhas, use sua imaginação e veja o que você pode criar.