Dados de caminho SVG no SkiaSharp
Definir caminhos usando strings de texto no formato Scalable Vector Graphics
A SKPath
classe dá suporte à definição de objetos de caminho inteiros de cadeias de caracteres de texto em um formato estabelecido pela especificação SVG (Scalable Vector Graphics). Você verá mais adiante neste artigo como pode representar um caminho inteiro como este em uma cadeia de caracteres de texto:
SVG é uma linguagem de programação gráfica baseada em XML para páginas da web. Como o SVG deve permitir que os caminhos sejam definidos na marcação em vez de uma série de chamadas de função, o padrão SVG inclui uma maneira extremamente concisa de especificar um caminho gráfico inteiro como uma cadeia de caracteres de texto.
No SkiaSharp, esse formato é chamado de "dados de caminho SVG". O formato também tem suporte em ambientes de programação baseados em XAML do Windows, incluindo o Windows Presentation Foundation e a Plataforma Universal do Windows, onde é conhecido como Sintaxe de Marcação de Caminho ou sintaxe de comandos Mover e desenhar. Ele também pode servir como um formato de troca para imagens gráficas vetoriais, particularmente em arquivos baseados em texto, como XML.
A SKPath
classe define dois métodos com as palavras SvgPathData
em seus nomes:
public static SKPath ParseSvgPathData(string svgPath)
public string ToSvgPathData()
O método estático ParseSvgPathData
converte uma cadeia de caracteres em um SKPath
objeto, enquanto ToSvgPathData
converte um SKPath
objeto em uma cadeia de caracteres.
Aqui está uma cadeia de caracteres SVG para uma estrela de cinco pontas centralizada no ponto (0, 0) com um raio de 100:
"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"
As letras são comandos que constroem um SKPath
objeto: M
indica uma MoveTo
chamada, L
é LineTo
, e Z
é Close
para fechar um contorno. Cada par de números fornece uma coordenada X e Y de um ponto. Observe que o L
comando é seguido por vários pontos separados por vírgulas. Em uma série de coordenadas e pontos, vírgulas e espaços em branco são tratados de forma idêntica. Alguns programadores preferem colocar vírgulas entre as coordenadas X e Y em vez de entre os pontos, mas vírgulas ou espaços são necessários apenas para evitar ambiguidade. Isso é perfeitamente legal:
"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"
A sintaxe dos dados de caminho SVG está formalmente documentada na Seção 8.3 da especificação SVG. Aqui está um resumo:
MoverTo
M x y
Isso inicia um novo contorno no caminho, definindo a posição atual. Os dados do caminho sempre devem começar com um M
comando.
LinhaPara
L x y ...
Este comando adiciona uma linha reta (ou linhas) ao caminho e define a nova posição atual para o final da última linha. Você pode seguir o L
comando com vários pares de coordenadas x e y .
Linha horizontalPara
H x ...
Este comando adiciona uma linha horizontal ao caminho e define a nova posição atual para o final da linha. Você pode seguir o H
comando com várias coordenadas x , mas não faz muito sentido.
Linha vertical
V y ...
Este comando adiciona uma linha vertical ao caminho e define a nova posição atual para o final da linha.
Fechar
Z
O C
comando fecha o contorno adicionando uma linha reta da posição atual até o início do contorno.
ArcTo
O comando para adicionar um arco elíptico ao contorno é de longe o comando mais complexo em toda a especificação de dados de caminho SVG. É o único comando no qual os números podem representar algo diferente de valores de coordenadas:
A rx ry rotation-angle large-arc-flag sweep-flag x y ...
Os parâmetros rx e ry são os raios horizontais e verticais da elipse. O ângulo de rotação é no sentido horário em graus.
Defina o sinalizador de arco grande como 1 para o arco grande ou como 0 para o arco pequeno.
Defina o sinalizador de varredura para 1 no sentido horário e para 0 no sentido anti-horário.
O arco é desenhado para o ponto (x, y), que se torna a nova posição atual.
CubicTo
C x1 y1 x2 y2 x3 y3 ...
Este comando adiciona uma curva de Bézier cúbica da posição atual para (x3, y3), que se torna a nova posição atual. Os pontos (x1, y1) e (x2, y2) são pontos de controle.
Várias curvas de Bézier podem ser especificadas por um único C
comando. O número de pontos deve ser um múltiplo de 3.
Há também um comando de curva de Bézier "suave":
S x2 y2 x3 y3 ...
Este comando deve seguir um comando Bézier regular (embora isso não seja estritamente necessário). O comando de Bézier suave calcula o primeiro ponto de controle para que seja um reflexo do segundo ponto de controle do Bézier anterior em torno de seu ponto mútuo. Esses três pontos são, portanto, colineares e a conexão entre as duas curvas de Bézier é suave.
QuadTo
Q x1 y1 x2 y2 ...
Para curvas de Bézier quadráticas, o número de pontos deve ser um múltiplo de 2. O ponto de controle é (x1, y1) e o ponto final (e nova posição atual) é (x2, y2)
Há também um comando de curva quadrática suave:
T x2 y2 ...
O ponto de controle é calculado com base no ponto de controle da curva quadrática anterior.
Todos esses comandos também estão disponíveis em versões "relativas", onde os pontos de coordenadas são relativos à posição atual. Esses comandos relativos começam com letras minúsculas, por exemplo c
, em vez C
da versão relativa do comando cúbico de Bézier.
Essa é a extensão da definição de dados de caminho SVG. Não há facilidade para repetir grupos de comandos ou para realizar qualquer tipo de cálculo. Comandos para ConicTo
ou outros tipos de especificações de arco não estão disponíveis.
O método estático SKPath.ParseSvgPathData
espera uma cadeia de caracteres válida de comandos SVG. Se algum erro de sintaxe for detectado, o método retornará null
. Essa é a única indicação de erro.
O ToSvgPathData
método é útil para obter dados de caminho SVG de um objeto existente SKPath
para transferir para outro programa ou para armazenar em um formato de arquivo baseado em texto, como XML. (O ToSvgPathData
método não é demonstrado no código de exemplo neste artigo.) Não espere ToSvgPathData
retornar uma cadeia de caracteres correspondente exatamente às chamadas de método que criaram o caminho. Em particular, você descobrirá que os arcos são convertidos em vários QuadTo
comandos e é assim que eles aparecem nos dados de caminho retornados do ToSvgPathData
.
A página Path Data Hello soletra a palavra "HELLO" usando dados de caminho SVG. SKPath
Os objetos e SKPaint
são definidos como campos na PathDataHelloPage
classe:
public class PathDataHelloPage : ContentPage
{
SKPath helloPath = SKPath.ParseSvgPathData(
"M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100" + // H
"M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100" + // E
"M 150 0 L 150 100, 200 100" + // L
"M 225 0 L 225 100, 275 100" + // L
"M 300 50 A 25 50 0 1 0 300 49.9 Z"); // O
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 10,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
...
}
O caminho que define a string de texto começa no canto superior esquerdo no point(0, 0). Cada letra tem 50 unidades de largura e 100 unidades de altura, e as letras são separadas por outras 25 unidades, o que significa que todo o caminho tem 350 unidades de largura.
O 'H' de "Olá" é composto por três contornos unifilares, enquanto o 'E' são duas curvas de Bézier cúbicas conectadas. Observe que o C
comando é seguido por seis pontos, e dois dos pontos de controle têm coordenadas Y de –10 e 110, o que os coloca fora do intervalo das coordenadas Y das outras letras. O 'L' são duas linhas conectadas, enquanto o 'O' é uma elipse que é renderizada com um A
comando.
Observe que o M
comando que inicia o último contorno define a posição para o ponto (350, 50), que é o centro vertical do lado esquerdo do 'O'. Conforme indicado pelos primeiros números após o A
comando, a elipse tem um raio horizontal de 25 e um raio vertical de 50. O ponto final é indicado pelo último par de números no A
comando, que representa o ponto (300, 49,9). Isso é deliberadamente apenas um pouco diferente do ponto inicial. Se o ponto final for definido como igual ao ponto inicial, o arco não será renderizado. Para desenhar uma elipse completa, você deve definir o ponto final próximo (mas não igual) ao ponto inicial ou deve usar dois ou mais A
comandos, cada um para parte da elipse completa.
Talvez você queira adicionar a seguinte instrução ao construtor da página e, em seguida, definir um ponto de interrupção para examinar a cadeia de caracteres resultante:
string str = helloPath.ToSvgPathData();
Você descobrirá que o arco foi substituído por uma longa série de Q
comandos para uma aproximação fragmentada do arco usando curvas quadráticas de Bézier.
O PaintSurface
manipulador obtém os limites apertados do caminho, que não incluem os pontos de controle para as curvas 'E' e 'O'. As três transformações movem o centro do caminho para o ponto (0, 0), dimensionam o caminho para o tamanho da tela (mas também levando em conta a largura do traço) e, em seguida, movem o centro do caminho para o centro da tela:
public class PathDataHelloPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect bounds;
helloPath.GetTightBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(info.Width / (bounds.Width + paint.StrokeWidth),
info.Height / (bounds.Height + paint.StrokeWidth));
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPath(helloPath, paint);
}
}
O caminho preenche a tela, o que parece mais razoável quando visualizado no modo paisagem:
A página Path Data Cat é semelhante. Os objetos path e paint são definidos como campos na PathDataCatPage
classe:
public class PathDataCatPage : ContentPage
{
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 paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Orange,
StrokeWidth = 5
};
...
}
A cabeça de um gato é um círculo, e aqui é renderizada com dois A
comandos, cada um dos quais desenha um semicírculo. Ambos os A
comandos para a cabeça definem raios horizontais e verticais de 100. O primeiro arco começa em (240, 100) e termina em (240, 300), que se torna o ponto inicial para o segundo arco que termina em (240, 100).
Os dois olhos também são renderizados com dois A
comandos e, como acontece com a cabeça do gato, o segundo A
comando termina no mesmo ponto que o início do primeiro A
comando. No entanto, esses pares de A
comandos não definem uma elipse. O com de cada arco é de 40 unidades e o raio também é de 40 unidades, o que significa que esses arcos não são semicírculos completos.
O PaintSurface
manipulador executa transformações semelhantes às do exemplo anterior, mas define um único Scale
fator para manter a taxa de proporção e fornecer uma pequena margem para que os bigodes do gato não toquem nas laterais da tela:
public class PathDataCatPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
SKRect bounds;
catPath.GetBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.9f * Math.Min(info.Width / bounds.Width,
info.Height / bounds.Height));
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPath(catPath, paint);
}
}
Este é o programa em execução:
Normalmente, quando um SKPath
objeto é definido como um campo, os contornos do caminho devem ser definidos no construtor ou em outro método. Ao usar dados de caminho SVG, no entanto, você viu que o caminho pode ser especificado inteiramente na definição de campo.
O exemplo anterior do Ugly Analog Clock no artigo The Rotate Transform exibia os ponteiros do relógio como linhas simples. O programa Pretty Analog Clock abaixo substitui essas linhas por SKPath
objetos definidos como campos na PrettyAnalogClockPage
classe junto com SKPaint
objetos:
public class PrettyAnalogClockPage : ContentPage
{
...
// Clock hands pointing straight up
SKPath hourHandPath = SKPath.ParseSvgPathData(
"M 0 -60 C 0 -30 20 -30 5 -20 L 5 0" +
"C 5 7.5 -5 7.5 -5 0 L -5 -20" +
"C -20 -30 0 -30 0 -60 Z");
SKPath minuteHandPath = SKPath.ParseSvgPathData(
"M 0 -80 C 0 -75 0 -70 2.5 -60 L 2.5 0" +
"C 2.5 5 -2.5 5 -2.5 0 L -2.5 -60" +
"C 0 -70 0 -75 0 -80 Z");
SKPath secondHandPath = SKPath.ParseSvgPathData(
"M 0 10 L 0 -80");
// SKPaint objects
SKPaint handStrokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 2,
StrokeCap = SKStrokeCap.Round
};
SKPaint handFillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Gray
};
...
}
Os ponteiros das horas e minutos agora têm áreas fechadas. Para tornar essas mãos distintas umas das outras, elas são desenhadas com um contorno preto e um preenchimento cinza usando os handStrokePaint
objetos e handFillPaint
.
Na amostra anterior do Ugly Analog Clock , os pequenos círculos que marcavam as horas e os minutos foram desenhados em um loop. Neste exemplo de Relógio Analógico Bonito, uma abordagem totalmente diferente é usada: as marcas de hora e minuto são linhas pontilhadas desenhadas com os minuteMarkPaint
objetos e hourMarkPaint
:
public class PrettyAnalogClockPage : ContentPage
{
...
SKPaint minuteMarkPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 3,
StrokeCap = SKStrokeCap.Round,
PathEffect = SKPathEffect.CreateDash(new float[] { 0, 3 * 3.14159f }, 0)
};
SKPaint hourMarkPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 6,
StrokeCap = SKStrokeCap.Round,
PathEffect = SKPathEffect.CreateDash(new float[] { 0, 15 * 3.14159f }, 0)
};
...
}
O artigo Pontos e traços discutiu como você pode usar o SKPathEffect.CreateDash
método para criar uma linha tracejada. O primeiro argumento é uma float
matriz que geralmente tem dois elementos: o primeiro elemento é o comprimento dos traços e o segundo elemento é a lacuna entre os traços. Quando a StrokeCap
propriedade é definida como SKStrokeCap.Round
, as extremidades arredondadas do traço alongam efetivamente o comprimento do traço pela largura do traço em ambos os lados do traço. Assim, definir o primeiro elemento de matriz como 0 cria uma linha pontilhada.
A distância entre esses pontos é governada pelo segundo elemento da matriz. Como você verá em breve, esses dois SKPaint
objetos são usados para desenhar círculos com um raio de 90 unidades. A circunferência deste círculo é, portanto, 180π, o que significa que as marcas de 60 minutos devem aparecer a cada 3π unidades, que é o segundo valor na float
matriz em minuteMarkPaint
. As marcas de 12 horas devem aparecer a cada 15π unidades, que é o valor na segunda float
matriz.
A PrettyAnalogClockPage
classe define um temporizador para invalidar a superfície a cada 16 milissegundos e o PaintSurface
manipulador é chamado nessa taxa. As definições anteriores dos SKPath
objetos e SKPaint
permitem um código de desenho muito limpo:
public class PrettyAnalogClockPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Transform for 100-radius circle in center
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(Math.Min(info.Width / 200, info.Height / 200));
// Draw circles for hour and minute marks
SKRect rect = new SKRect(-90, -90, 90, 90);
canvas.DrawOval(rect, minuteMarkPaint);
canvas.DrawOval(rect, hourMarkPaint);
// Get time
DateTime dateTime = DateTime.Now;
// Draw hour hand
canvas.Save();
canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
canvas.DrawPath(hourHandPath, handStrokePaint);
canvas.DrawPath(hourHandPath, handFillPaint);
canvas.Restore();
// Draw minute hand
canvas.Save();
canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
canvas.DrawPath(minuteHandPath, handStrokePaint);
canvas.DrawPath(minuteHandPath, handFillPaint);
canvas.Restore();
// Draw second hand
double t = dateTime.Millisecond / 1000.0;
if (t < 0.5)
{
t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
}
else
{
t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
}
canvas.Save();
canvas.RotateDegrees(6 * (dateTime.Second + (float)t));
canvas.DrawPath(secondHandPath, handStrokePaint);
canvas.Restore();
}
}
Algo especial é feito com o ponteiro dos segundos, no entanto. Como o relógio é atualizado a cada 16 milissegundos, a Millisecond
DateTime
propriedade do valor pode ser usada para animar um ponteiro de segundos de varredura em vez de um que se move em saltos discretos de segundo para segundo. Mas este código não permite que o movimento seja suave. Em vez disso, ele usa as funções de atenuação de Xamarin.FormsSpringIn
animação e SpringOut
para um tipo diferente de movimento. Essas funções de atenuação fazem com que o ponteiro dos segundos se mova de maneira mais brusca - recuando um pouco antes de se mover e, em seguida, ultrapassando ligeiramente seu destino, um efeito que infelizmente não pode ser reproduzido nessas capturas de tela estáticas: