Enumeração e informações de caminho
Obter informações sobre caminhos e enumerar o conteúdo
A SKPath
classe define várias propriedades e métodos que permitem obter informações sobre o caminho. As Bounds
propriedades e TightBounds
(e métodos relacionados) obtêm as dimensões métricas de um caminho. O Contains
método permite determinar se um ponto específico está dentro de um caminho.
Às vezes, é útil determinar o comprimento total de todas as linhas e curvas que compõem um caminho. Calcular esse comprimento não é uma tarefa algoritmicamente simples, então uma classe inteira nomeada PathMeasure
é dedicada a ele.
Às vezes, também é útil obter todas as operações de desenho e pontos que compõem um caminho. A princípio, essa facilidade pode parecer desnecessária: se o seu programa criou o caminho, o programa já conhece o conteúdo. No entanto, você viu que os caminhos também podem ser criados por efeitos de caminho e pela conversão de cadeias de caracteres de texto em caminhos. Você também pode obter todas as operações de desenho e pontos que compõem esses caminhos. Uma possibilidade é aplicar uma transformação algorítmica a todos os pontos, por exemplo, para quebrar o texto em torno de um hemisfério:
Obtendo o comprimento do caminho
No artigo Caminhos e texto , você viu como usar o DrawTextOnPath
método para desenhar uma cadeia de caracteres de texto cuja linha de base segue o curso de um caminho. Mas e se você quiser dimensionar o texto para que ele se encaixe no caminho com precisão? Desenhar texto em torno de um círculo é fácil porque a circunferência de um círculo é simples de calcular. Mas a circunferência de uma elipse ou o comprimento de uma curva de Bézier não é tão simples.
A SKPathMeasure
turma pode ajudar. O construtor aceita um SKPath
argumento e a Length
propriedade revela seu comprimento.
Essa classe é demonstrada no exemplo Path Length , que se baseia na página Bezier Curve . O arquivo PathLengthPage.xaml deriva de e inclui uma interface de InteractivePage
toque:
<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.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
Title="Path Length">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</local:InteractivePage>
O arquivo code-behind PathLengthPage.xaml.cs permite mover quatro pontos de contato para definir os pontos finais e os pontos de controle de uma curva cúbica de Bézier. Três campos definem uma cadeia de caracteres de texto, um SKPaint
objeto e uma largura calculada do texto:
public partial class PathLengthPage : InteractivePage
{
const string text = "Compute length of path";
static SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Black,
TextSize = 10,
};
static readonly float baseTextWidth = textPaint.MeasureText(text);
...
}
O baseTextWidth
campo é a largura do texto com base em uma TextSize
configuração de 10.
O PaintSurface
manipulador desenha a curva de Bézier e, em seguida, dimensiona o texto para caber ao longo de todo o seu comprimento:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw path with cubic Bezier curve
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.CubicTo(touchPoints[1].Center,
touchPoints[2].Center,
touchPoints[3].Center);
canvas.DrawPath(path, strokePaint);
// Get path length
SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
// Find new text size
textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
// Draw text on path
canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
}
...
}
A Length
propriedade do objeto recém-criado SKPathMeasure
obtém o comprimento do caminho. O comprimento do baseTextWidth
caminho é dividido pelo valor (que é a largura do texto com base em um tamanho de texto de 10) e, em seguida, multiplicado pelo tamanho do texto base de 10. O resultado é um novo tamanho de texto para exibir o texto ao longo desse caminho:
À medida que a curva de Bézier fica mais longa ou mais curta, você pode ver o tamanho do texto mudar.
Atravessando o Caminho
SKPathMeasure
pode fazer mais do que apenas medir o comprimento do caminho. Para qualquer valor entre zero e o comprimento do caminho, um SKPathMeasure
objeto pode obter a posição no caminho e a tangente à curva do caminho nesse ponto. A tangente está disponível como um vetor na forma de um SKPoint
objeto, ou como uma rotação encapsulada em um SKMatrix
objeto. Aqui estão os métodos de obtenção dessas SKPathMeasure
informações de maneiras variadas e flexíveis:
Boolean GetPosition (Single distance, out SKPoint position)
Boolean GetTangent (Single distance, out SKPoint tangent)
Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)
Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)
Os membros da SKPathMeasureMatrixFlags
enumeração são:
GetPosition
GetTangent
GetPositionAndTangent
A página Unicycle Half-Pipe anima uma figura de vara em um monociclo que parece andar de um lado para o outro ao longo de uma curva cúbica de Bézier:
O SKPaint
objeto usado para acariciar o half-pipe e o monociclo é definido como um campo na UnicycleHalfPipePage
classe. Também está definido o SKPath
objeto para o monociclo:
public class UnicycleHalfPipePage : ContentPage
{
...
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
SKPath unicyclePath = SKPath.ParseSvgPathData(
"M 0 0" +
"A 25 25 0 0 0 0 -50" +
"A 25 25 0 0 0 0 0 Z" +
"M 0 -25 L 0 -100" +
"A 15 15 0 0 0 0 -130" +
"A 15 15 0 0 0 0 -100 Z" +
"M -25 -85 L 25 -85");
...
}
A classe contém as substituições padrão dos OnAppearing
métodos e OnDisappearing
para animação. O PaintSurface
manipulador cria o caminho para o half-pipe e, em seguida, desenha-o. Um SKPathMeasure
objeto é então criado com base neste caminho:
public class UnicycleHalfPipePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath pipePath = new SKPath())
{
pipePath.MoveTo(50, 50);
pipePath.CubicTo(0, 1.25f * info.Height,
info.Width - 0, 1.25f * info.Height,
info.Width - 50, 50);
canvas.DrawPath(pipePath, strokePaint);
using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
{
float length = pathMeasure.Length;
// Animate t from 0 to 1 every three seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 5 / 5);
// t from 0 to 1 to 0 but slower at beginning and end
t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
SKMatrix matrix;
pathMeasure.GetMatrix(t * length, out matrix,
SKPathMeasureMatrixFlags.GetPositionAndTangent);
canvas.SetMatrix(matrix);
canvas.DrawPath(unicyclePath, strokePaint);
}
}
}
}
O PaintSurface
manipulador calcula um valor t
que vai de 0 a 1 a cada cinco segundos. Em seguida, ele usa a Math.Cos
função para converter isso em um valor t
que varia de 0 a 1 e volta para 0, onde 0 corresponde ao monociclo no início no canto superior esquerdo, enquanto 1 corresponde ao monociclo no canto superior direito. A função cosseno faz com que a velocidade seja mais lenta na parte superior do tubo e mais rápida na parte inferior.
Observe que esse valor de deve ser multiplicado pelo comprimento do t
caminho para o primeiro argumento para GetMatrix
. A matriz é então aplicada ao SKCanvas
objeto para desenhar o caminho do monociclo.
Enumerando o caminho
Duas classes incorporadas de SKPath
permitem que você enumere o conteúdo do caminho. Essas classes são SKPath.Iterator
e SKPath.RawIterator
. As duas classes são muito semelhantes, mas SKPath.Iterator
podem eliminar elementos no caminho com um comprimento zero, ou perto de um comprimento zero. O RawIterator
é usado no exemplo abaixo.
Você pode obter um objeto do tipo SKPath.RawIterator
chamando o CreateRawIterator
método de SKPath
. A enumeração através do caminho é realizada chamando repetidamente o Next
método. Passe para ele uma matriz de quatro SKPoint
valores:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
O Next
método retorna um membro do SKPathVerb
tipo de enumeração. Esses valores indicam o comando de desenho específico no caminho. O número de pontos válidos inseridos na matriz depende deste verbo:
Move
com um único pontoLine
com dois pontosCubic
com quatro pontosQuad
com três pontosConic
com três pontos (e também chamar oConicWeight
método para o peso)Close
com um pontoDone
O Done
verbo indica que a enumeração de caminho está completa.
Observe que não Arc
há verbos. Isso indica que todos os arcos são convertidos em curvas de Bézier quando adicionados ao caminho.
Algumas das informações na SKPoint
matriz são redundantes. Por exemplo, se um Move
verbo é seguido por um Line
verbo, então o primeiro dos dois pontos que acompanham o Line
é o mesmo que o Move
ponto. Na prática, essa redundância é muito útil. Quando você recebe um Cubic
verbo, ele é acompanhado por todos os quatro pontos que definem a curva cúbica de Bézier. Você não precisa manter a posição atual estabelecida pelo verbo anterior.
O verbo problemático, no entanto, é Close
. Este comando traça uma linha reta da posição atual até o início do contorno estabelecido anteriormente pelo Move
comando. Idealmente, o verbo Close
deveria fornecer esses dois pontos em vez de apenas um ponto. O pior é que o ponto que acompanha o verbo Close
é sempre (0, 0). Ao enumerar através de um caminho, você provavelmente precisará reter o Move
ponto e a posição atual.
Enumeração, achatamento e malformação
Às vezes, é desejável aplicar uma transformação algorítmica a um caminho para malformá-lo de alguma forma:
A maioria dessas letras consiste em linhas retas, mas essas linhas retas aparentemente foram torcidas em curvas. Como isso é possível?
A chave é que as linhas retas originais são quebradas em uma série de linhas retas menores. Essas linhas retas menores individuais podem então ser manipuladas de diferentes maneiras para formar uma curva.
Para ajudar nesse processo, o exemplo contém uma classe estática PathExtensions
com um Interpolate
método que divide uma linha reta em várias linhas curtas que têm apenas uma unidade de comprimento. Além disso, a classe contém vários métodos que convertem os três tipos de curvas de Bézier em uma série de minúsculas linhas retas que se aproximam da curva. (As fórmulas paramétricas foram apresentadas no artigo Três tipos de curvas de Bézier.) Esse processo é chamado de achatamento da curva:
static class PathExtensions
{
...
static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
{
int count = (int)Math.Max(1, Length(pt0, pt1));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * pt0.X + t * pt1.X;
float y = (1 - t) * pt0.Y + t * pt1.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
3 * t * (1 - t) * (1 - t) * pt1.X +
3 * t * t * (1 - t) * pt2.X +
t * t * t * pt3.X;
float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
3 * t * (1 - t) * (1 - t) * pt1.Y +
3 * t * t * (1 - t) * pt2.Y +
t * t * t * pt3.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
x /= denominator;
y /= denominator;
points[i] = new SKPoint(x, y);
}
return points;
}
static double Length(SKPoint pt0, SKPoint pt1)
{
return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
}
}
Todos esses métodos são referenciados a partir do método CloneWithTransform
de extensão também incluído nesta classe e mostrado abaixo. Esse método clona um caminho enumerando os comandos path e construindo um novo caminho com base nos dados. No entanto, o novo caminho consiste apenas em MoveTo
e LineTo
chamadas. Todas as curvas e retas são reduzidas a uma série de linhas minúsculas.
Ao chamar CloneWithTransform
, você passa para o método a Func<SKPoint, SKPoint>
, que é uma função com um SKPaint
parâmetro que retorna um SKPoint
valor. Essa função é chamada para cada ponto para aplicar uma transformação algorítmica personalizada:
static class PathExtensions
{
public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
{
SKPath pathOut = new SKPath();
using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
{
SKPoint[] points = new SKPoint[4];
SKPathVerb pathVerb = SKPathVerb.Move;
SKPoint firstPoint = new SKPoint();
SKPoint lastPoint = new SKPoint();
while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
{
switch (pathVerb)
{
case SKPathVerb.Move:
pathOut.MoveTo(transform(points[0]));
firstPoint = lastPoint = points[0];
break;
case SKPathVerb.Line:
SKPoint[] linePoints = Interpolate(points[0], points[1]);
foreach (SKPoint pt in linePoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[1];
break;
case SKPathVerb.Cubic:
SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
foreach (SKPoint pt in cubicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[3];
break;
case SKPathVerb.Quad:
SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
foreach (SKPoint pt in quadPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Conic:
SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
foreach (SKPoint pt in conicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Close:
SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
foreach (SKPoint pt in closePoints)
{
pathOut.LineTo(transform(pt));
}
firstPoint = lastPoint = new SKPoint(0, 0);
pathOut.Close();
break;
}
}
}
return pathOut;
}
...
}
Como o caminho clonado é reduzido a linhas retas minúsculas, a função transform tem a capacidade de converter linhas retas em curvas.
Observe que o método retém o primeiro ponto de cada contorno na variável chamada firstPoint
e a posição atual após cada comando de desenho na variável lastPoint
. Essas variáveis são necessárias para construir a linha de fechamento final quando um Close
verbo é encontrado.
O exemplo GlobularText usa esse método de extensão para aparentemente quebrar texto ao redor de um hemisfério em um efeito 3D:
O GlobularTextPage
construtor de classe executa essa transformação. Ele cria um SKPaint
objeto para o texto e, em seguida, obtém um SKPath
objeto do GetTextPath
método. Este é o caminho passado para o método de CloneWithTransform
extensão junto com uma função de transformação:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
public GlobularTextPage()
{
Title = "Globular Text";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
using (SKPaint textPaint = new SKPaint())
{
textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
textPaint.TextSize = 100;
using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
{
SKRect textPathBounds;
textPath.GetBounds(out textPathBounds);
globePath = textPath.CloneWithTransform((SKPoint pt) =>
{
double longitude = (Math.PI / textPathBounds.Width) *
(pt.X - textPathBounds.Left) - Math.PI / 2;
double latitude = (Math.PI / textPathBounds.Height) *
(pt.Y - textPathBounds.Top) - Math.PI / 2;
longitude *= 0.75;
latitude *= 0.75;
float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
float y = (float)Math.Sin(latitude);
return new SKPoint(x, y);
});
}
}
}
...
}
A função transform primeiro calcula dois valores nomeados longitude
e latitude
que variam de –π/2 na parte superior e esquerda do texto, a π/2 à direita e na parte inferior do texto. O intervalo desses valores não é visualmente satisfatório, então eles são reduzidos multiplicando-se por 0,75. (Experimente o código sem esses ajustes. O texto torna-se demasiado obscuro nos polos norte e sul, e demasiado fino nas laterais.) Essas coordenadas esféricas tridimensionais são convertidas em coordenadas bidimensionais e y
bidimensionais x
por fórmulas padrão.
O novo caminho é armazenado como um campo. O PaintSurface
manipulador, então, só precisa centralizar e dimensionar o caminho para exibi-lo na tela:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint pathPaint = new SKPaint())
{
pathPaint.Style = SKPaintStyle.Fill;
pathPaint.Color = SKColors.Blue;
pathPaint.StrokeWidth = 3;
pathPaint.IsAntialias = true;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
canvas.DrawPath(globePath, pathPaint);
}
}
}
Esta é uma técnica muito versátil. Se a matriz de efeitos de caminho descrita no artigo Efeitos de caminho não abranger algo que você achou que deveria ser incluído, essa é uma maneira de preencher as lacunas.