繪製弧形的三種方式
瞭解如何使用 SkiaSharp 以三種不同的方式定義弧線
弧線是橢圓形圓周的曲線,例如這個無限符號的四捨五入部分:
儘管該定義簡單,但無法定義滿足每個需求的弧形繪製函式,因此,在繪製弧線的最佳方法的圖形系統之間沒有共識。基於這個理由,類別 SKPath
不會只限制為一種方法。
SKPath
定義 AddArc
方法、五個不同的 ArcTo
方法和兩個相對 RArcTo
方法。 這些方法分為三個類別,代表三種非常不同的方法來指定弧線。您使用哪一個取決於可用來定義弧線的資訊,以及此弧線如何配合您要繪製的其他圖形。
角度弧形
繪製弧線的角度弧線方法需要您指定一個矩形來限定橢圓形。 這個橢圓形圓周的弧線是由橢圓形中央的角度來表示,該橢圓形的開頭及其長度。 兩種不同的方法繪製角度弧線。 這些是 AddArc
方法和 ArcTo
方法:
public void AddArc (SKRect oval, Single startAngle, Single sweepAngle)
public void ArcTo (SKRect oval, Single startAngle, Single sweepAngle, Boolean forceMoveTo)
這些方法與 Android 和 [ArcTo
]xref:Android.Graphics.Path.ArcToAddArc
*) 方法相同。 iOS AddArc
方法很類似,但僅限於圓周的弧線,而不是一般化為橢圓形。
這兩種方法都是以 SKRect
值開頭,定義橢圓形的位置和大小:
弧線是這個橢圓形圓周的一部分。
自 startAngle
變數是以度為單位的順時針角度,相對於從右邊橢圓形中央繪製的水平線。 自 sweepAngle
變數相對於 startAngle
。 以下是startAngle
sweepAngle
分別 60 度和 100 度的值:
弧線從開始角度開始。 其長度是由掃掠角度所控管。 弧線以紅色顯示:
使用 AddArc
或 ArcTo
方法新增至路徑的曲線只是橢圓形圓周的一部分:
startAngle
或 sweepAngle
自變數可以是負數:反時針為負值sweepAngle
,反時針方向為負值。
不過,AddArc
不會定義封閉的輪廓。 如果您在 之後呼叫 LineTo
,則會從弧線的結尾繪製到 方法中的LineTo
點,而 相同的 是 ArcTo
。AddArc
AddArc
會自動啟動新的輪廓,且在功能上相當於呼叫 ArcTo
,其最終自變數為 true
:
path.ArcTo (oval, startAngle, sweepAngle, true);
最後一個MoveTo
自變數稱為 forceMoveTo
,而且實際上會在弧線開頭造成呼叫。這開始新的輪廓。 這不是最後一個自變數 false
的情況:
path.ArcTo (oval, startAngle, sweepAngle, false);
這個版本的 ArcTo
會從目前位置到弧線的開頭繪製線條。這表示弧線可以是較大輪廓中間的某處。
Angle Arc 頁面可讓您使用兩個滑桿來指定開始和掃掠角度。 XAML 檔案會具現化兩Slider
個元素和 。SKCanvasView
PaintCanvas
AngleArcPage.xaml.cs檔案中的處理程式會使用定義為欄位的兩個SKPaint
物件來繪製橢圓形和弧線:
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);
}
}
如您所見,開始角度和掃掠角度都可以採用負值:
產生弧線的方法在演算法上是最簡單的方法,而且很容易衍生描述弧線的參數方程式。瞭解橢圓形的大小和位置,以及開始和掃掠角度,可以使用簡單的三角計算弧線的起點和終點:
x = oval.MidX + (oval.Width / 2) * cos(angle)
y = oval.MidY + (oval.Height / 2) * sin(angle)
值為 angle
startAngle
或 startAngle + sweepAngle
。
使用兩個角度來定義弧線最適合您知道您想要繪製之弧形的角度長度的情況,例如製作餅圖。 [ 爆炸式餅圖] 頁面示範這一點。 類別 ExplodedPieChartPage
會使用內部類別來定義一些捏造的數據和色彩:
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)
};
處理程式 PaintSurface
會先迴圈查看專案以計算 totalValues
數位。 從中,它可以將每個專案的大小判斷為總計的分數,並將它轉換成角度:
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;
}
}
系統會為每個餅圖配量建立新的 SKPath
物件。 路徑包含一條來自中央的線條,然後 ArcTo
是 繪製弧線,另一行則從呼叫傳回中心結果 Close
。 此程式會顯示「爆炸」餅圖配量,方法是將其全部從中心移出 50 圖元。 該工作需要向量,以每個配量掃掠角度的中間點方向:
若要查看它的外觀,沒有「爆炸」,只是評論 Translate
電話:
正切弧形
所支援SKPath
的第二個弧線類型是正切弧線,因此稱為,因為弧線是圓圈的周長,而圓線是兩條連接線的正切線。
正切圓弧線會新增至路徑,並呼叫 ArcTo
具有兩 SKPoint
個參數的方法,或 ArcTo
具有點不同 Single
參數的多載:
public void ArcTo (SKPoint point1, SKPoint point2, Single radius)
public void ArcTo (Single x1, Single y1, Single x2, Single y2, Single radius)
這個 ArcTo
方法類似於 PostScript arct
(第 532 頁) 函式和 iOS AddArcToPoint
方法。
方法 ArcTo
包含三點:
- 輪廓的目前點,如果
MoveTo
尚未呼叫,則為點 (0, 0) - 方法的第一個 point 自變數
ArcTo
,稱為 角點 - 的第二個點自變數
ArcTo
,稱為 目的地點:
這三個點會定義兩條連接線:
如果三個點是粗線,也就是說,如果他們位於相同的直線上,就不會繪製任何弧線。
方法 ArcTo
也包含 radius
參數。 這會定義圓形的半徑:
橢圓形不會將正切弧線一般化。
如果這兩條線在任何角度相遇,可以在這些線條之間插入該圓形,使其與這兩行正切:
加入至輪廓的曲線不會觸碰 方法中指定的 ArcTo
任何一個點。 它包含從目前點到第一個正切點的直線,以及以紅色顯示於第二個正切點的弧線:
以下是新增至輪廓的最後直線和弧線:
輪廓可以從第二個正切點繼續。
[ 正切弧度] 頁面可讓您實驗正切弧線。這是衍生自 InteractivePage
的數個頁面中的第一個頁面,它會定義一些方便 SKPaint
的物件並執行 TouchPoint
處理:
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();
}
}
}
TangentArcPage
類別衍生自 InteractivePage
。 TangentArcPage.xaml.cs 檔案中的建構函式負責具現化和初始化touchPoints
數位,並將 (inInteractivePage
) 設定baseCanvasView
為 SKCanvasView
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();
}
}
...
}
處理程式 PaintSurface
會 ArcTo
使用 方法來根據觸控點和 Slider
繪製弧線,但也會以演算法方式計算角度所依據的圓形:
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);
}
}
以下是執行中的 正切 Arc 頁面:
正切圓弧很適合用來建立圓角,例如圓角矩形。 因為 SKPath
已經包含 AddRoundedRect
方法, 圓角 Heptagon 頁面會示範如何使用 ArcTo
來四捨五入多邊形的角落。 (任何一般多邊形的程序代碼都已一般化。
類別 PaintSurface
的 RoundedHeptagonPage
處理程式包含一個 for
循環來計算 Heptagon 七個頂點的座標,而第二個則計算這些頂點中點的七端。 接著會使用這些中間點來建構路徑:
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);
}
}
}
以下是程式執行情況:
橢圓形弧形
橢圓形弧線會新增至路徑,並呼叫 ArcTo
具有兩 SKPoint
個參數的方法,或 ArcTo
具有個別 X 和 Y 座標的多載:
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)
橢圓形弧線與可調整向量圖形 (SVG) 和 通用 Windows 平台 ArcSegment
類別中包含的橢圓弧線一致。
這些 ArcTo
方法會在兩個點之間繪製弧線,也就是輪廓的目前點,以及方法的最後一個參數 ArcTo
( xy
參數或個別 x
和 y
參數):
方法的第一個 point 參數 ArcTo
(r
或 和 rx
ry
) 根本不是一個點,而是指定橢圓形的水準和垂直弧度;
參數 xAxisRotate
是旋轉這個橢圓形的順時針度數:
如果接著放置這個傾斜橢圓形,使其觸碰兩個點,則點會由兩個不同的弧線連接:
這兩個弧線可以透過兩種方式來區分:頂端弧線大於底部弧線,而且當弧線從左到右繪製時,上方弧線會以順時針方向繪製,而底部弧線則以逆時針方向繪製。
您也可以以另一種方式調整兩點之間的橢圓形:
現在頂部有一個較小的弧線,繪製順時針,而底部的較大弧線則以逆時針繪製。
因此,這兩個點可以透過傾斜橢圓形所定義的弧線連接,總共有四種方式:
這四個弧線會以 和 SKPathDirection
列舉型別自變數ArcTo
的四個組合SKPathArcSize
來區分方法:
- red:SKPathArcSize.Large 和 SKPathDirection.Clockwise
- 綠色:SKPathArcSize.Small 和 SKPathDirection.Clockwise
- blue:SKPathArcSize.Small 和 SKPathDirection.CounterClockwise
- magenta:SKPathArcSize.Large 和 SKPathDirection.CounterClockwise
如果傾斜橢圓形不夠大,無法容納兩點之間,則會統一縮放,直到夠大為止。 在該案例中,只有兩個唯一的弧線連接兩個點。 這些可以與 SKPathDirection
參數區別。
雖然這種方法在第一次遇到時定義弧線聽起來很複雜,但它是唯一允許使用旋轉橢圓形來定義弧線的方法,而且當您需要整合弧線與其他輪廓部分時,通常是最簡單的方法。
[ 橢圓形弧 形] 頁面可讓您以互動方式設定兩個點,以及橢圓形的大小和旋轉。 類別EllipticalArcPage
衍生自 InteractivePage
,而PaintSurface
EllipticalArcPage.xaml.cs程式代碼後置檔案中的處理程式會繪製四個弧線:
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);
}
}
這裡正在執行:
Arc Infinity 頁面會使用橢圓形弧線繪製無限號。 無限號是以兩個圓形為基礎,其半徑為100單位,並以100個單位分隔:
兩條交叉彼此的兩條線與兩個圓形相切:
無限號是這些圓形和兩行部分的組合。 若要使用橢圓形弧線繪製無限正負號,必須判斷兩條線與圓形正切的座標。
在其中一個圓形中建構右矩形:
圓圈的半徑為100個單位,而三角形的虛構為150個單位,因此角度α是100的反正弦值,除以150或41.8度。 三角形另一邊的長度是 41.8 度或 112 度的餘弦 150 倍,也可以由 Pythagorean 定理計算。
接著可以使用這項資訊來計算正切點的座標:
x = 112·cos(41.8) = 83
y = 112·sin(41.8) = 75
這四個正切點都是繪製以點 (0, 0, 0) 為圓弧度為 100 的無限正切符號所需的一切:
類別 PaintSurface
中的 ArcInfinityPage
處理程式會放置無限號,讓 (0, 0) 點位於頁面中央,並將路徑調整為螢幕大小:
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);
}
}
}
程序代碼會 Bounds
使用 的 屬性 SKPath
來判斷無限正弦的維度,將其調整為畫布的大小:
結果似乎有點小,這表示 Bounds
的 SKPath
屬性報告的大小大於路徑。
在內部,Skia 會使用多個二次方貝塞爾曲線來近似弧線。 這些曲線(如下一節所示)包含控制點,可控管曲線的繪製方式,但不是轉譯曲線的一部分。 屬性 Bounds
包含這些控制點。
若要取得更緊密的大小,請使用 TightBounds
屬性,該屬性會排除控制點。 以下是以橫向模式執行的程式,並使用 TightBounds
屬性來取得路徑界限:
雖然弧線與直線之間的連接在數學上是平滑的,但從弧線到直線的變化似乎有點突然。 下一篇關於 貝氏曲線三種類型的文章中會顯示一個更好的無限符號。