共用方式為


繪製弧形的三種方式

瞭解如何使用 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。 以下是startAnglesweepAngle分別 60 度和 100 度的值:

定義角度弧線的角度

弧線從開始角度開始。 其長度是由掃掠角度所控管。 弧線以紅色顯示:

反白顯示的角度弧線

使用 AddArcArcTo 方法新增至路徑的曲線只是橢圓形圓周的一部分:

角度弧線本身

startAnglesweepAngle 自變數可以是負數:反時針為負值sweepAngle,反時針方向為負值。

不過,AddArc不會定義封閉的輪廓。 如果您在 之後呼叫 LineTo ,則會從弧線的結尾繪製到 方法中的LineTo點,而 相同的 是 ArcToAddArc

AddArc 會自動啟動新的輪廓,且在功能上相當於呼叫 ArcTo ,其最終自變數為 true

path.ArcTo (oval, startAngle, sweepAngle, true);

最後一個MoveTo自變數稱為 forceMoveTo,而且實際上會在弧線開頭造成呼叫。這開始新的輪廓。 這不是最後一個自變數 false的情況:

path.ArcTo (oval, startAngle, sweepAngle, false);

這個版本的 ArcTo 會從目前位置到弧線的開頭繪製線條。這表示弧線可以是較大輪廓中間的某處。

Angle Arc 頁面可讓您使用兩個滑桿來指定開始和掃掠角度。 XAML 檔案會具現化兩Slider個元素和 。SKCanvasView PaintCanvasAngleArcPage.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);
    }
}

如您所見,開始角度和掃掠角度都可以採用負值:

Angle Arc 頁面的三重螢幕快照

產生弧線的方法在演算法上是最簡單的方法,而且很容易衍生描述弧線的參數方程式。瞭解橢圓形的大小和位置,以及開始和掃掠角度,可以使用簡單的三角計算弧線的起點和終點:

x = oval.MidX + (oval.Width / 2) * cos(angle)

y = oval.MidY + (oval.Height / 2) * sin(angle)

值為 angle startAnglestartAngle + 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) 設定baseCanvasViewSKCanvasView 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();
        }
    }
    ...
}

處理程式 PaintSurfaceArcTo 使用 方法來根據觸控點和 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 來四捨五入多邊形的角落。 (任何一般多邊形的程序代碼都已一般化。

類別 PaintSurfaceRoundedHeptagonPage 處理程式包含一個 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);
        }
    }
}

以下是程式執行情況:

四捨五入 Heptagon 頁面的三個螢幕快照

橢圓形弧形

橢圓形弧線會新增至路徑,並呼叫 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 方法會在兩個點之間繪製弧線,也就是輪廓的目前點,以及方法的最後一個參數 ArcToxy 參數或個別 xy 參數):

定義橢圓形弧線的兩個點

方法的第一個 point 參數 ArcTor或 和 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,而PaintSurfaceEllipticalArcPage.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);
    }
}

這裡正在執行:

Elliptical Arc 頁面的三個螢幕快照

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 來判斷無限正弦的維度,將其調整為畫布的大小:

Arc Infinity 頁面的三重螢幕快照

結果似乎有點小,這表示 BoundsSKPath 屬性報告的大小大於路徑。

在內部,Skia 會使用多個二次方貝塞爾曲線來近似弧線。 這些曲線(如下一節所示)包含控制點,可控管曲線的繪製方式,但不是轉譯曲線的一部分。 屬性 Bounds 包含這些控制點。

若要取得更緊密的大小,請使用 TightBounds 屬性,該屬性會排除控制點。 以下是以橫向模式執行的程式,並使用 TightBounds 屬性來取得路徑界限:

具有緊密界限的 Arc Infinity 頁面三重螢幕快照

雖然弧線與直線之間的連接在數學上是平滑的,但從弧線到直線的變化似乎有點突然。 下一篇關於 貝氏曲線三種類型的文章中會顯示一個更好的無限符號。