Três formas de desenhar um arco
Saiba como usar o SkiaSharp para definir arcos de três maneiras diferentes
Um arco é uma curva na circunferência de uma elipse, como as partes arredondadas deste signo do infinito:
Apesar da simplicidade dessa definição, não há como definir uma função de desenho de arco que satisfaça todas as necessidades e, portanto, não há consenso entre os sistemas gráficos da melhor maneira de desenhar um arco. Por isso, a SKPath
classe não se restringe a apenas uma abordagem.
SKPath
define um AddArc
método, cinco métodos diferentes ArcTo
e dois métodos relativos RArcTo
. Esses métodos se enquadram em três categorias, representando três abordagens muito diferentes para especificar um arco. Qual deles você usa depende das informações disponíveis para definir o arco e como esse arco se encaixa com os outros gráficos que você está desenhando.
O Arco do Ângulo
A abordagem do arco angular para desenhar arcos requer que você especifique um retângulo que limite uma elipse. O arco na circunferência desta elipse é indicado por ângulos do centro da elipse que indicam o início do arco e seu comprimento. Dois métodos diferentes desenham arcos angulares. Estes são o AddArc
método e o ArcTo
método:
public void AddArc (SKRect oval, Single startAngle, Single sweepAngle)
public void ArcTo (SKRect oval, Single startAngle, Single sweepAngle, Boolean forceMoveTo)
Esses métodos são idênticos aos métodos Android AddArc
e [ArcTo
]xref:Android.Graphics.Path.ArcTo*). O método iOS AddArc
é semelhante, mas é restrito a arcos na circunferência de um círculo, em vez de generalizado para uma elipse.
Ambos os métodos começam com um SKRect
valor que define o local e o tamanho de uma elipse:
O arco é uma parte da circunferência desta elipse.
O startAngle
argumento é um ângulo no sentido horário em graus em relação a uma linha horizontal desenhada do centro da elipse à direita. O sweepAngle
argumento é relativo ao startAngle
. Aqui estão startAngle
e sweepAngle
valores de 60 graus e 100 graus, respectivamente:
O arco começa no ângulo inicial. Seu comprimento é regido pelo ângulo de varredura. O arco é mostrado aqui em vermelho:
A curva adicionada ao caminho com o AddArc
método or ArcTo
é simplesmente a parte da circunferência da elipse:
Os startAngle
argumentos ou sweepAngle
podem ser negativos: O arco é no sentido horário para valores positivos de sweepAngle
e anti-horário para valores negativos.
No entanto, AddArc
não define um contorno fechado. Se você chamar LineTo
depois AddArc
, uma linha é desenhada do final do arco até o ponto no LineTo
método, e o mesmo é verdadeiro para ArcTo
.
AddArc
inicia automaticamente um novo contorno e é funcionalmente equivalente a uma chamada para ArcTo
com um argumento final de true
:
path.ArcTo (oval, startAngle, sweepAngle, true);
Esse último argumento é chamado forceMoveTo
de , e efetivamente causa uma MoveTo
chamada no início do arco. Começa assim um novo contorno. Não é o caso de um último argumento de false
:
path.ArcTo (oval, startAngle, sweepAngle, false);
Esta versão do ArcTo
traça uma linha da posição atual até o início do arco. Isso significa que o arco pode estar em algum lugar no meio de um contorno maior.
A página Arco Angular permite usar dois controles deslizantes para especificar os ângulos de início e varredura. O arquivo XAML instancia dois Slider
elementos e um SKCanvasView
arquivo . O PaintCanvas
manipulador no arquivo AngleArcPage.xaml.cs desenha o oval e o arco usando dois SKPaint
objetos definidos como campos:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
float startAngle = (float)startAngleSlider.Value;
float sweepAngle = (float)sweepAngleSlider.Value;
canvas.DrawOval(rect, outlinePaint);
using (SKPath path = new SKPath())
{
path.AddArc(rect, startAngle, sweepAngle);
canvas.DrawPath(path, arcPaint);
}
}
Como você pode ver, tanto o ângulo inicial quanto o ângulo de varredura podem assumir valores negativos:
Essa abordagem para gerar um arco é algoritmicamente a mais simples, e é fácil derivar as equações paramétricas que descrevem o arco. Sabendo o tamanho e a localização da elipse, e os ângulos de início e varredura, os pontos inicial e final do arco podem ser calculados usando trigonometria simples:
x = oval.MidX + (oval.Width / 2) * cos(angle)
y = oval.MidY + (oval.Height / 2) * sin(angle)
O angle
valor é ou startAngle
startAngle + sweepAngle
.
O uso de dois ângulos para definir um arco é melhor para casos em que você sabe o comprimento angular do arco que deseja desenhar, por exemplo, para fazer um gráfico de pizza. A página Gráfico de pizza explodido demonstra isso. A ExplodedPieChartPage
classe usa uma classe interna para definir alguns dados e cores fabricados:
class ChartData
{
public ChartData(int value, SKColor color)
{
Value = value;
Color = color;
}
public int Value { private set; get; }
public SKColor Color { private set; get; }
}
ChartData[] chartData =
{
new ChartData(45, SKColors.Red),
new ChartData(13, SKColors.Green),
new ChartData(27, SKColors.Blue),
new ChartData(19, SKColors.Magenta),
new ChartData(40, SKColors.Cyan),
new ChartData(22, SKColors.Brown),
new ChartData(29, SKColors.Gray)
};
O PaintSurface
manipulador primeiro faz um loop pelos itens para calcular um totalValues
número. A partir disso, ele pode determinar o tamanho de cada item como a fração do total e convertê-lo em um ângulo:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int totalValues = 0;
foreach (ChartData item in chartData)
{
totalValues += item.Value;
}
SKPoint center = new SKPoint(info.Width / 2, info.Height / 2);
float explodeOffset = 50;
float radius = Math.Min(info.Width / 2, info.Height / 2) - 2 * explodeOffset;
SKRect rect = new SKRect(center.X - radius, center.Y - radius,
center.X + radius, center.Y + radius);
float startAngle = 0;
foreach (ChartData item in chartData)
{
float sweepAngle = 360f * item.Value / totalValues;
using (SKPath path = new SKPath())
using (SKPaint fillPaint = new SKPaint())
using (SKPaint outlinePaint = new SKPaint())
{
path.MoveTo(center);
path.ArcTo(rect, startAngle, sweepAngle, false);
path.Close();
fillPaint.Style = SKPaintStyle.Fill;
fillPaint.Color = item.Color;
outlinePaint.Style = SKPaintStyle.Stroke;
outlinePaint.StrokeWidth = 5;
outlinePaint.Color = SKColors.Black;
// Calculate "explode" transform
float angle = startAngle + 0.5f * sweepAngle;
float x = explodeOffset * (float)Math.Cos(Math.PI * angle / 180);
float y = explodeOffset * (float)Math.Sin(Math.PI * angle / 180);
canvas.Save();
canvas.Translate(x, y);
// Fill and stroke the path
canvas.DrawPath(path, fillPaint);
canvas.DrawPath(path, outlinePaint);
canvas.Restore();
}
startAngle += sweepAngle;
}
}
Um novo SKPath
objeto é criado para cada fatia de pizza. O caminho consiste em uma linha do centro, depois uma ArcTo
para desenhar o arco e outra linha de volta para o centro resulta da Close
chamada. Este programa exibe fatias de torta "explodidas", movendo-as todas para fora do centro por 50 pixels. Essa tarefa requer um vetor na direção do ponto médio do ângulo de varredura para cada fatia:
Para ver como fica sem a "explosão", basta comentar a Translate
chamada:
O Arco Tangente
O segundo tipo de arco suportado por SKPath
é o arco tangente, assim chamado porque o arco é a circunferência de um círculo que é tangente a duas linhas conectadas.
Um arco tangente é adicionado a um caminho com uma chamada para o ArcTo
método com dois SKPoint
parâmetros, ou a ArcTo
sobrecarga com parâmetros separados Single
para os pontos:
public void ArcTo (SKPoint point1, SKPoint point2, Single radius)
public void ArcTo (Single x1, Single y1, Single x2, Single y2, Single radius)
Esse ArcTo
método é semelhante à função PostScript arct
(página 532) e ao método iOS AddArcToPoint
.
O ArcTo
método envolve três pontos:
- O ponto atual do contorno, ou o ponto (0, 0) se
MoveTo
não tiver sido chamado - O primeiro argumento de ponto para o
ArcTo
método, chamado de ponto de canto - O segundo argumento de ponto para
ArcTo
, chamou o ponto de destino:
Estes três pontos definem duas linhas conectadas:
Se os três pontos forem colineares — isto é, se estiverem na mesma reta — nenhum arco será desenhado.
O ArcTo
método também inclui um radius
parâmetro. Isso define o raio de um círculo:
O arco tangente não é generalizado para uma elipse.
Se as duas linhas se encontrarem em qualquer ângulo, esse círculo pode ser inserido entre essas linhas de modo que seja tangente a ambas as linhas:
A curva adicionada ao contorno não toca em nenhum dos pontos especificados no ArcTo
método. Consiste em uma linha reta do ponto atual até o primeiro ponto tangente, e um arco que termina no segundo ponto tangente, mostrado aqui em vermelho:
Aqui está a reta final e o arco que é adicionado ao contorno:
O contorno pode ser continuado a partir do segundo ponto tangente.
A página Arco Tangente permite que você experimente o arco tangente. Esta é a primeira de várias páginas que derivam do InteractivePage
, que define alguns objetos úteis SKPaint
e executa TouchPoint
o processamento:
public class InteractivePage : ContentPage
{
protected SKCanvasView baseCanvasView;
protected TouchPoint[] touchPoints;
protected SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 3
};
protected SKPaint redStrokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 15
};
protected SKPaint dottedStrokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 3,
PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
};
protected void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
bool touchPointMoved = false;
foreach (TouchPoint touchPoint in touchPoints)
{
float scale = baseCanvasView.CanvasSize.Width / (float)baseCanvasView.Width;
SKPoint point = new SKPoint(scale * (float)args.Location.X,
scale * (float)args.Location.Y);
touchPointMoved |= touchPoint.ProcessTouchEvent(args.Id, args.Type, point);
}
if (touchPointMoved)
{
baseCanvasView.InvalidateSurface();
}
}
}
A classe TangentArcPage
deriva de InteractivePage
. O construtor no arquivo TangentArcPage.xaml.cs é responsável por instanciar e inicializar a touchPoints
matriz e definir baseCanvasView
(em InteractivePage
) para o SKCanvasView
objeto instanciado no arquivo TangentArcPage.xaml:
public partial class TangentArcPage : InteractivePage
{
public TangentArcPage()
{
touchPoints = new TouchPoint[3];
for (int i = 0; i < 3; i++)
{
TouchPoint touchPoint = new TouchPoint
{
Center = new SKPoint(i == 0 ? 100 : 500,
i != 2 ? 100 : 500)
};
touchPoints[i] = touchPoint;
}
InitializeComponent();
baseCanvasView = canvasView;
radiusSlider.Value = 100;
}
void sliderValueChanged(object sender, ValueChangedEventArgs args)
{
if (canvasView != null)
{
canvasView.InvalidateSurface();
}
}
...
}
O PaintSurface
manipulador usa o ArcTo
método para desenhar o arco com base nos pontos de toque e um Slider
, mas também calcula algoritmicamente o círculo no qual o ângulo é baseado:
public partial class TangentArcPage : InteractivePage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw the two lines that meet at an angle
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.LineTo(touchPoints[1].Center);
path.LineTo(touchPoints[2].Center);
canvas.DrawPath(path, dottedStrokePaint);
}
// Draw the circle that the arc wraps around
float radius = (float)radiusSlider.Value;
SKPoint v1 = Normalize(touchPoints[0].Center - touchPoints[1].Center);
SKPoint v2 = Normalize(touchPoints[2].Center - touchPoints[1].Center);
double dotProduct = v1.X * v2.X + v1.Y * v2.Y;
double angleBetween = Math.Acos(dotProduct);
float hypotenuse = radius / (float)Math.Sin(angleBetween / 2);
SKPoint vMid = Normalize(new SKPoint((v1.X + v2.X) / 2, (v1.Y + v2.Y) / 2));
SKPoint center = new SKPoint(touchPoints[1].Center.X + vMid.X * hypotenuse,
touchPoints[1].Center.Y + vMid.Y * hypotenuse);
canvas.DrawCircle(center.X, center.Y, radius, this.strokePaint);
// Draw the tangent arc
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.ArcTo(touchPoints[1].Center, touchPoints[2].Center, radius);
canvas.DrawPath(path, redStrokePaint);
}
foreach (TouchPoint touchPoint in touchPoints)
{
touchPoint.Paint(canvas);
}
}
// Vector methods
SKPoint Normalize(SKPoint v)
{
float magnitude = Magnitude(v);
return new SKPoint(v.X / magnitude, v.Y / magnitude);
}
float Magnitude(SKPoint v)
{
return (float)Math.Sqrt(v.X * v.X + v.Y * v.Y);
}
}
Aqui está a página Tangent Arc em execução:
O arco tangente é ideal para criar cantos arredondados, como um retângulo arredondado. Como SKPath
já inclui um AddRoundedRect
método, a página Heptagon arredondado demonstra como usar ArcTo
para arredondar os cantos de um polígono de sete lados. (O código é generalizado para qualquer polígono regular.)
O PaintSurface
manipulador da RoundedHeptagonPage
classe contém um for
loop para calcular as coordenadas dos sete vértices do heptagon, e um segundo para calcular os pontos médios dos sete lados a partir desses vértices. Esses pontos médios são usados para construir o caminho:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float cornerRadius = 100;
int numVertices = 7;
float radius = 0.45f * Math.Min(info.Width, info.Height);
SKPoint[] vertices = new SKPoint[numVertices];
SKPoint[] midPoints = 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;
}
// Coordinates of the midpoints of the sides connecting the vertices
for (int vertex = 0; vertex < numVertices; vertex++)
{
int prevVertex = (vertex + numVertices - 1) % numVertices;
midPoints[vertex] = new SKPoint((vertices[prevVertex].X + vertices[vertex].X) / 2,
(vertices[prevVertex].Y + vertices[vertex].Y) / 2);
}
// Create the path
using (SKPath path = new SKPath())
{
// Begin at the first midpoint
path.MoveTo(midPoints[0]);
for (int vertex = 0; vertex < numVertices; vertex++)
{
SKPoint nextMidPoint = midPoints[(vertex + 1) % numVertices];
// Draws a line from the current point, and then the arc
path.ArcTo(vertices[vertex], nextMidPoint, cornerRadius);
// Connect the arc with the next midpoint
path.LineTo(nextMidPoint);
}
path.Close();
// 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;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawPath(path, paint);
}
}
}
Este é o programa em execução:
O Arco Elíptico
O arco elíptico é adicionado a um caminho com uma chamada para o ArcTo
método que tem dois SKPoint
parâmetros, ou a ArcTo
sobrecarga com coordenadas X e Y separadas:
public void ArcTo (SKPoint r, Single xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy)
public void ArcTo (Single rx, Single ry, Single xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, Single x, Single y)
O arco elíptico é consistente com o arco elíptico incluído no SVG (Scalable Vector Graphics) e na classe Universal Windows Platform ArcSegment
.
Esses ArcTo
métodos desenham um arco entre dois pontos, que são o ponto atual do contorno, e o último parâmetro para o ArcTo
método (o xy
parâmetro ou o separado x
e y
parâmetros):
O primeiro parâmetro de ponto para o ArcTo
método (r
, ou rx
e ry
) não é um ponto, mas especifica os raios horizontais e verticais de uma elipse;
O xAxisRotate
parâmetro é o número de graus no sentido horário para girar essa elipse:
Se essa elipse inclinada for então posicionada de modo que toque os dois pontos, os pontos serão conectados por dois arcos diferentes:
Esses dois arcos podem ser distinguidos de duas maneiras: O arco superior é maior do que o arco inferior, e como o arco é desenhado da esquerda para a direita, o arco superior é desenhado no sentido horário, enquanto o arco inferior é desenhado no sentido anti-horário.
Também é possível encaixar a elipse entre os dois pontos de outra forma:
Agora há um arco menor na parte superior que é desenhado no sentido horário, e um arco maior na parte inferior que é desenhado no sentido anti-horário.
Esses dois pontos podem, portanto, ser conectados por um arco definido pela elipse inclinada em um total de quatro maneiras:
Esses quatro arcos são distinguidos pelas quatro combinações dos argumentos do SKPathArcSize
tipo e SKPathDirection
enumeração para o ArcTo
método:
- vermelho: SKPathArcSize.Large e SKPathDirection.Clockwise
- verde: SKPathArcSize.Small e SKPathDirection.Clockwise
- azul: SKPathArcSize.Small e SKPathDirection.CounterClockwise
- magenta: SKPathArcSize.Large e SKPathDirection.CounterClockwise
Se a elipse inclinada não for grande o suficiente para caber entre os dois pontos, então ela é uniformemente dimensionada até que seja grande o suficiente. Apenas dois arcos únicos conectam os dois pontos nesse caso. Estes podem ser distinguidos com o SKPathDirection
parâmetro.
Embora essa abordagem para definir um arco pareça complexa no primeiro encontro, é a única abordagem que permite definir um arco com uma elipse rotacionada, e geralmente é a abordagem mais fácil quando você precisa integrar arcos com outras partes do contorno.
A página Arco Elíptico permite que você defina interativamente os dois pontos e o tamanho e a rotação da elipse. A EllipticalArcPage
classe deriva de InteractivePage
, e o PaintSurface
manipulador no arquivo code-behind EllipticalArcPage.xaml.cs desenha os quatro arcos:
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())
{
int colorIndex = 0;
SKPoint ellipseSize = new SKPoint((float)xRadiusSlider.Value,
(float)yRadiusSlider.Value);
float rotation = (float)rotationSlider.Value;
foreach (SKPathArcSize arcSize in Enum.GetValues(typeof(SKPathArcSize)))
foreach (SKPathDirection direction in Enum.GetValues(typeof(SKPathDirection)))
{
path.MoveTo(touchPoints[0].Center);
path.ArcTo(ellipseSize, rotation,
arcSize, direction,
touchPoints[1].Center);
strokePaint.Color = colors[colorIndex++];
canvas.DrawPath(path, strokePaint);
path.Reset();
}
}
foreach (TouchPoint touchPoint in touchPoints)
{
touchPoint.Paint(canvas);
}
}
Aqui está em execução:
A página Arc Infinity usa o arco elíptico para desenhar um sinal de infinito. O sinal do infinito é baseado em dois círculos com raios de 100 unidades separados por 100 unidades:
Duas linhas que se cruzam são tangente aos dois círculos:
O signo do infinito é uma combinação de partes desses círculos e das duas linhas. Para usar o arco elíptico para desenhar o sinal do infinito, as coordenadas onde as duas linhas são tangente aos círculos devem ser determinadas.
Construa um retângulo reto em um dos círculos:
O raio do círculo é de 100 unidades, e a hipotenusa do triângulo é de 150 unidades, então o ângulo α é o arco seno (senoe inverso) de 100 dividido por 150, ou 41,8 graus. O comprimento do outro lado do triângulo é 150 vezes o cosseno de 41,8 graus, ou 112, que também pode ser calculado pelo teorema de Pitágoras.
As coordenadas do ponto tangente podem então ser calculadas usando estas informações:
x = 112·cos(41.8) = 83
y = 112·sin(41.8) = 75
Os quatro pontos tangentes são tudo o que é necessário para desenhar um sinal de infinito centrado no ponto (0, 0) com raios circulares de 100:
O PaintSurface
manipulador na ArcInfinityPage
classe posiciona o sinal de infinito para que o ponto (0, 0) seja posicionado no centro da página e dimensiona o caminho para o tamanho da tela:
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.LineTo(83, 75);
path.ArcTo(100, 100, 0, SKPathArcSize.Large, SKPathDirection.CounterClockwise, 83, -75);
path.LineTo(-83, 75);
path.ArcTo(100, 100, 0, SKPathArcSize.Large, SKPathDirection.Clockwise, -83, -75);
path.Close();
// Use path.TightBounds for coordinates without control points
SKRect pathBounds = path.Bounds;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(Math.Min(info.Width / pathBounds.Width,
info.Height / pathBounds.Height));
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Blue;
paint.StrokeWidth = 5;
canvas.DrawPath(path, paint);
}
}
}
O código usa a Bounds
propriedade de SKPath
para determinar as dimensões do seno infinito para dimensioná-lo para o tamanho da tela:
O resultado parece um pouco pequeno, o que sugere que a Bounds
propriedade de SKPath
está relatando um tamanho maior do que o caminho.
Internamente, Skia aproxima o arco usando múltiplas curvas quadráticas de Bézier. Essas curvas (como você verá na próxima seção) contêm pontos de controle que governam como a curva é desenhada, mas não fazem parte da curva renderizada. A Bounds
propriedade inclui esses pontos de controle.
Para obter um ajuste mais apertado, use a TightBounds
propriedade, que exclui os pontos de controle. Aqui está o programa em execução no modo paisagem e usando a TightBounds
propriedade para obter os limites de caminho:
Embora as conexões entre os arcos e as linhas retas sejam matematicamente suaves, a mudança de arco para linha reta pode parecer um pouco abrupta. Um melhor sinal do infinito é apresentado no próximo artigo sobre Três tipos de curvas de Bézier.