共用方式為


SkiaSharp 中的 SVG 路徑數據

使用可調整向量圖形格式的文字字串定義路徑

類別 SKPath 支援以可調整向量圖形 (SVG) 規格所建立的格式,從文字字串定義整個路徑物件。 您稍後會在本文中看到如何代表整個路徑,例如文字字串中的這個路徑:

使用 SVG 路徑資料定義的範例路徑

SVG 是網頁的 XML 圖形程式設計語言。 由於 SVG 必須允許在標記中定義路徑,而不是一系列函數調用,SVG 標準包含非常簡潔的方式,將整個圖形路徑指定為文字字串。

在 SkiaSharp 中,此格式稱為「SVG 路徑數據」。Windows XAML 型程式設計環境中也支援此格式,包括 Windows Presentation Foundation 和 通用 Windows 平台,其稱為路徑標記語法移動和繪製命令語法。 它也可以做為向量圖形影像的交換格式,特別是在 XML 之類的文字型檔案中。

類別 SKPath 會使用其名稱中的單字 SvgPathData 定義兩種方法:

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

靜態 ParseSvgPathData 方法會將字串 SKPath 轉換成 物件,同時 ToSvgPathData 將 對象轉換成 SKPath 字串。

以下是以點 (0, 0, 0) 為半徑為 100 的五點星形的 SVG 字串:

"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"

字母是建置 SKPath 物件的命令: M 表示 MoveTo 呼叫、 LLineTo,而且 ZClose 關閉輪廓。 每個數位組都會提供點的 X 和 Y 座標。 請注意, L 命令後面接著以逗號分隔的多個點。 在一系列的座標和點中,會以相同的方式處理逗號和空格符。 有些程式設計人員偏好在 X 和 Y 座標之間放置逗號,而不是在點之間放置逗號,但只需要逗號或空格,以避免模棱兩可。 這是完全合法的:

"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"

SVG 路徑數據的語法已正式記載於 SVG 規格的第 8.3 節。 以下是摘要:

MoveTo

M x y

這會藉由設定目前的位置,在路徑中開始新的輪廓。 路徑數據應該一律以 M 命令開頭。

LineTo

L x y ...

此命令會將直線(或線條)新增至路徑,並將新的目前位置設定為最後一行的結尾。 您可以使用多個 xy 座標組來遵循 L 命令。

水準LineTo

H x ...

此命令會將水準線新增至路徑,並將新的目前位置設定為行尾。 您可以使用多個 x 座標來追蹤H命令,但意義不大。

垂直線

V y ...

此命令會將垂直線新增至路徑,並將新的目前位置設定為行尾。

關閉

Z

命令會 C 藉由將直線從目前位置新增至輪廓的開頭,來關閉輪廓。

ArcTo

將橢圓形弧線新增至輪廓的命令,是整個SVG路徑數據規格中最複雜的命令。 這是唯一的命令,數位可以代表座標值以外的值:

A rx ry rotation-angle large-arc-flag sweep-flag x y ...

rxry 參數是橢圓形的水準和垂直弧度。 旋轉角度以度為單位順時針方向。

大型弧形旗標 設定為 1,或將小弧線設定為 0。

將掃掠旗標設定為 1,以順時針順時針設定為 0。

弧線繪製到點 (xy),這會成為新的目前位置。

CubicTo

C x1 y1 x2 y2 x3 y3 ...

此命令會將立方貝塞爾曲線從目前位置新增至 (x3y3),這會成為新的目前位置。 點 (x1y1) 和 (x2y2) 是控制點。

單一 C 命令可以指定多個 Bézier 曲線。 點數必須是 3 的倍數。

還有一個「平滑」貝塞爾曲線命令:

S x2 y2 x3 y3 ...

此命令應該遵循一般的 Bézier 命令(雖然這不是絕對必要)。 Smooth Bézier 命令會計算第一個控制點,使其反映前一個貝塞爾第二個控制點在其相互點周圍。 因此,這三個點是粗線的,兩個貝塞爾曲線之間的連接是平滑的。

QuadTo

Q x1 y1 x2 y2 ...

對於二次方貝塞爾曲線,點數必須是 2 的倍數。 控制點為 (x1y1) 和終點 (以及新的目前位置) 為 (x2y2

也有平滑二次曲線命令:

T x2 y2 ...

控制點是根據上一個二次曲線的控制點來計算。

所有這些命令也適用於「相對」版本,其中座標點相對於目前的位置。 這些相對命令會以小寫字母開頭,例如 c ,而不是 C 立方貝塞爾命令的相對版本。

這是 SVG 路徑資料定義的範圍。 沒有重複命令群組或執行任何類型的計算的功能。 無法使用 或 ConicTo 其他類型的 Arc 規格命令。

靜態 SKPath.ParseSvgPathData 方法需要有效的 SVG 命令字串。 如果偵測到任何語法錯誤,方法會傳 null回 。 這是唯一的錯誤指示。

此方法 ToSvgPathData 適用於從現有 SKPath 物件取得 SVG 路徑數據,以傳送至另一個程式,或儲存在文字型檔格式,例如 XML。 (本文ToSvgPathData中的範例程式代碼未示範 方法。請勿預期ToSvgPathData會傳回對應至建立路徑之方法呼叫的字串。 特別是,您會發現弧線會轉換成多個 QuadTo 命令,這就是它們出現在從 ToSvgPathData傳回的路徑數據中的方式。

Path Data Hello 頁面會使用 SVG 路徑數據來拼出 「HELLO」 這個字。 SKPathSKPaint 物件都會定義為類別中的PathDataHelloPage欄位:

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
    };
    ...
}

定義文字字串的路徑會從點(0, 0) 的左上角開始。 每個字母寬 50 單位,高 100 個單位,字母則以另外 25 個單位分隔,這表示整個路徑寬 350 個單位。

“Hello” 的 'H' 由三條一線輪廓組成,而 'E' 則是兩條連接的立方貝塞爾曲線。 請注意, C 命令後面接著六個點,而其中兩個控制點的 Y 座標為 –10 和 110,這會將它們置於其他字母的 Y 座標範圍內。 'L' 是兩條連接線,而 'O' 是使用 A 命令轉譯的省略號。

請注意 M ,開始最後一個輪廓的命令會將位置設定為點 (350, 50),這是 『O』 左邊的垂直中心。 如命令後面的 A 第一個數位所指示,橢圓形的水準半徑為25,垂直半徑為50。 結束點是由命令中的 A 最後一對數位表示,代表點 (300, 49.9)。 這故意與起點稍有不同。 如果端點設定為等於起點,則不會轉譯弧線。 若要繪製完整的橢圓形,您必須將端點設定為接近 (但不等於) 起點,或者您必須使用兩個以上的 A 命令,每個命令都是完整橢圓形的一部分。

您可能會想要將下列語句新增至頁面的建構函式,然後設定斷點來檢查結果字串:

string str = helloPath.ToSvgPathData();

您會發現弧線已取代成一連串 Q 的命令,以使用二次方貝塞爾曲線進行弧形的分次近似值。

處理程式 PaintSurface 會取得路徑的緊密界限,這不包含 『E』 和 『O』 曲線的控制點。 這三個轉換會將路徑的中心移至點 (0, 0),將路徑調整為畫布的大小(但也考慮筆劃寬度),然後將路徑的中心移至畫布的中心:

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);
    }
}

路徑會填滿畫布,在橫向模式中檢視時看起來更合理:

路徑數據 Hello 頁面的三重螢幕快照

[ 路徑數據貓 ] 頁面很類似。 路徑與繪製物件都定義為類別中的 PathDataCatPage 欄位:

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 個命令轉譯,每一個命令繪製半圓形。 前端的兩 A 個命令都會定義水準和垂直弧度為100。 第一個弧線從(240,100)開始,結尾為(240,300),成為第二個弧線結束的起點(240,100)。

兩個眼睛也會使用兩 A 個命令來轉譯,和貓的頭部一樣,第二個 A 命令的結尾與第一個 A 命令的開頭相同。 不過,這些配對的 A 命令不會定義橢圓形。 每個弧線的 是 40 個單位,半徑也是 40 個單位,這表示這些弧線不是完整的半圓形。

處理程式 PaintSurface 會執行與上一個範例類似的轉換,但設定單 Scale 一因素來維持外觀比例,並提供一點邊界,讓貓的鬍鬚不會觸碰螢幕的兩側:

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);
    }
}

以下是程式執行情況:

[路徑數據貓] 頁面的三重螢幕快照

一般而言,當物件定義為字段時 SKPath ,路徑的輪廓必須在建構函式或其他方法中定義。 不過,使用 SVG 路徑數據時,您已看到路徑可以在欄位定義中完全指定。

「旋轉轉換」文章中先前的Ugly類比時鐘範例會將時鐘的手顯示為簡單的線條。 以下的美式模擬時鐘程式會將這些行SKPath取代為 類別中PrettyAnalogClockPage欄位的物件以及 SKPaint 物件:

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
    };
    ...
}

小時和分鐘手現在已封閉區域。 為了讓這些手彼此不同,它們會使用 handStrokePainthandFillPaint 物件,以黑色外框和灰色填滿繪製。

在先前的 醜陋類比時鐘 範例中,標記小時和分鐘的小圓圈是在迴圈中繪製的。 在這個漂亮的類比時鐘範例中,會使用完全不同的方法:小時和分鐘標記是以 和 hourMarkPaint 對象繪製minuteMarkPaint的虛線:

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)
    };
    ...
}

Dots 和 Dashes 文章討論了如何使用 SKPathEffect.CreateDash 方法來建立虛線。 第一個自變數是一般有兩個元素的陣列:第一個專案 float 是虛線的長度,而第二個元素則是破折號之間的間距。 StrokeCap當 屬性設定為 SKStrokeCap.Round時,虛線的四捨五入結尾會透過虛線兩側的筆劃寬度有效延長虛線長度。 因此,將第一個陣列元素設定為0會建立虛線。

這些點之間的距離是由第二個陣列元素所控管。 如您所見,這兩個 SKPaint 對象用來繪製半徑為90單位的圓形。 因此,這個圓形的周長為180π,這表示60分鐘標記必須每3π個單位出現一次,這是陣列minuteMarkPaint中的float第二個值。 12 小時標記必須每 15π 個單位出現一次,也就是第二 float 個陣列中的值。

類別會將 PrettyAnalogClockPage 定時器設定為每隔 16 毫秒使介面失效,並以 PaintSurface 該速率呼叫處理程式。 與 SKPaint 物件的先前定義SKPath允許非常乾淨的繪圖程式代碼:

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();
    }
}

然而,第二手做了一些特別的事情。 由於時鐘每 16 毫秒更新一次, Millisecond 因此值的屬性 DateTime 可能會用來以動畫顯示掃掠第二手,而不是以離散跳躍從第二個到第二個移動的指標。 但此程式代碼不允許移動順暢。 相反地,它會針對不同類型的移動使用 Xamarin.FormsSpringInSpringOut 動畫 Easing 函式。 這些 Easing 函式會讓第二手以 jerkier 的方式移動 — 在移動之前先回拉一點,然後稍微過度拍攝其目的地,但不幸的是,這些靜態螢幕快照無法重現的效果:

[漂亮的模擬時鐘] 頁面的三重螢幕快照