路徑資訊與列舉
取得路徑的相關信息並列舉內容
類別 SKPath
會定義數個屬性和方法,讓您取得路徑的相關信息。 Bounds
和 TightBounds
屬性 (和相關方法) 會取得路徑的計量維度。 Contains
方法可讓您判斷特定點是否在路徑內。
判斷組成路徑之所有線條和曲線的總長度有時很有用。 計算此長度不是演算法上簡單的工作,因此名為 PathMeasure
的整個類別都專門用來計算此長度。
有時候,取得組成路徑的所有繪圖作業和點也很有用。 一開始,此設施似乎不必要:如果您的程式已建立路徑,程式就已經知道內容。 不過,您已看到路徑效果也可以建立路徑,以及將文字字串轉換成路徑來建立路徑。 您也可以取得組成這些路徑的所有繪圖作業和點。 其中一種可能性是將演算法轉換套用至所有點,例如,將文字包裝在半球:
取得路徑長度
在 Path 和 Text 一文中,您已瞭解如何使用 DrawTextOnPath
方法來繪製其基準遵循路徑歷程的文字字串。 但是,如果您想要調整文字大小,使其完全符合路徑,該怎麼辦? 在圓形周圍繪製文字很容易,因為圓形的周長很容易計算。 但是橢圓形或貝塞爾曲線的長度不是那麼簡單。
類別 SKPathMeasure
可以協助。 建 構函式 接受自 SKPath
變數,而 Length
屬性會顯示其長度。
此類別會在路徑長度範例中示範,此範例是以 Bezier Curve 頁面為基礎。 PathLengthPage.xaml 檔案衍生自 InteractivePage
,並包含觸控介面:
<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SkiaSharpFormsDemos"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
Title="Path Length">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</local:InteractivePage>
PathLengthPage.xaml.cs程式代碼後置檔案可讓您移動四個觸控點,以定義立方貝塞爾曲線的終點和控制點。 三個 SKPaint
欄位會定義文字字串、物件和文字的匯出寬度:
public partial class PathLengthPage : InteractivePage
{
const string text = "Compute length of path";
static SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Black,
TextSize = 10,
};
static readonly float baseTextWidth = textPaint.MeasureText(text);
...
}
欄位 baseTextWidth
是以 10 設定為基礎的 TextSize
文字寬度。
處理程式 PaintSurface
會繪製 Bézier 曲線,然後調整文字大小以符合其完整長度:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw path with cubic Bezier curve
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.CubicTo(touchPoints[1].Center,
touchPoints[2].Center,
touchPoints[3].Center);
canvas.DrawPath(path, strokePaint);
// Get path length
SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
// Find new text size
textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
// Draw text on path
canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
}
...
}
Length
新建立SKPathMeasure
物件的 屬性會取得路徑的長度。 路徑長度會除 baseTextWidth
以值(這是以文字大小 10 為基礎的文字寬度),然後乘以 10 的基底文字大小。 結果是一個新的文字大小,用來顯示沿著該路徑的文字:
當貝塞爾曲線變長或較短時,您可以看到文字大小變更。
周遊路徑
SKPathMeasure
可以不只是測量路徑的長度。 對於介於零和路徑長度之間的任何值, SKPathMeasure
物件可以取得路徑上的位置,以及該點路徑曲線的正切值。 正切值是以物件的形式 SKPoint
提供,或做為封裝在 物件中的 SKMatrix
旋轉形式。 以下是以各種且彈性方式取得此資訊的方法 SKPathMeasure
:
Boolean GetPosition (Single distance, out SKPoint position)
Boolean GetTangent (Single distance, out SKPoint tangent)
Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)
Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)
列舉的成員 SKPathMeasureMatrixFlags
包括:
GetPosition
GetTangent
GetPositionAndTangent
單 輪半管 頁面在單輪車上動畫顯示一個棍子圖,似乎沿著立方貝塞爾曲線來回騎:
SKPaint
用於撫摸半管和單輪車的物件會定義為 類別中的UnicycleHalfPipePage
欄位。 此外,定義也是 SKPath
單輪車的物件:
public class UnicycleHalfPipePage : ContentPage
{
...
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
SKPath unicyclePath = SKPath.ParseSvgPathData(
"M 0 0" +
"A 25 25 0 0 0 0 -50" +
"A 25 25 0 0 0 0 0 Z" +
"M 0 -25 L 0 -100" +
"A 15 15 0 0 0 0 -130" +
"A 15 15 0 0 0 0 -100 Z" +
"M -25 -85 L 25 -85");
...
}
類別包含動畫之 OnAppearing
和 OnDisappearing
方法的標準覆寫。 處理程式 PaintSurface
會建立半管線的路徑,然後繪製它。 SKPathMeasure
然後會根據這個路徑建立 物件:
public class UnicycleHalfPipePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath pipePath = new SKPath())
{
pipePath.MoveTo(50, 50);
pipePath.CubicTo(0, 1.25f * info.Height,
info.Width - 0, 1.25f * info.Height,
info.Width - 50, 50);
canvas.DrawPath(pipePath, strokePaint);
using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
{
float length = pathMeasure.Length;
// Animate t from 0 to 1 every three seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 5 / 5);
// t from 0 to 1 to 0 but slower at beginning and end
t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
SKMatrix matrix;
pathMeasure.GetMatrix(t * length, out matrix,
SKPathMeasureMatrixFlags.GetPositionAndTangent);
canvas.SetMatrix(matrix);
canvas.DrawPath(unicyclePath, strokePaint);
}
}
}
}
處理程式 PaintSurface
會計算每 5 秒從 0 到 1 的值 t
。 然後,它會使用 函 Math.Cos
式,將該值轉換成 t
範圍從 0 到 1,然後回到 0,其中 0 對應到左上方開頭的單輪車,而 1 對應到右上方的單輪車。 餘弦函數會使管道頂端的速度變慢,底部速度最快。
請注意,這個 值 t
必須乘以第一個自變數的路徑長度為 GetMatrix
。 矩陣接著會套用至 物件, SKCanvas
以繪製單輪路徑。
列舉路徑
的 SKPath
兩個內嵌類別可讓您列舉路徑的內容。 這些類別是 SKPath.Iterator
與 SKPath.RawIterator
。 這兩個類別非常類似,但 SKPath.Iterator
可以排除路徑中長度為零或接近零長度的專案。 RawIterator
在下列範例中使用 。
您可以呼叫 的 方法,以取得 型SKPath.RawIterator
別的物件SKPath
。CreateRawIterator
透過路徑列舉可透過重複呼叫 Next
方法來完成。 傳遞至四個值的陣列 SKPoint
:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
方法 Next
會傳回列舉型別的成員 SKPathVerb
。 這些值表示路徑中的特定繪圖命令。 插入陣列的有效點數目取決於這個動詞:
Move
具有單一點Line
具有兩個點Cubic
具有四分Quad
具有三個點Conic
具有三個點 (也呼叫ConicWeight
權數的方法)Close
具有一個點Done
動 Done
詞命令表示路徑列舉已完成。
請注意,沒有 Arc
動詞。 這表示當加入路徑時,所有弧線都會轉換成 Bézier 曲線。
陣列中的 SKPoint
部分資訊是多餘的。 例如,如果Move
動詞後面接著動詞,則伴隨 Line
的兩個點中的第一個Line
與點相同Move
。 在實務上,這種備援非常實用。 當您得到動 Cubic
詞時,它會伴隨著定義立方貝塞爾曲線的所有四個點。 您不需要保留上一個動詞所建立的目前位置。
不過,有問題的動詞命令是 Close
。 此命令會從目前位置繪製直線,到命令稍早建立 Move
的輪廓開頭。 在理想情況下, Close
動詞應該提供這兩個點,而不只是一個點。 更糟的是,伴隨動詞的點 Close
總是 (0, 0)。 當您列舉路徑時,您可能需要保留 Move
點和目前的位置。
列舉、扁平化和格式不正確
有時候,最好將演算法轉換套用至路徑,以某種方式使其格式不正確:
這些字母大多由直線組成,但這些直線顯然被扭曲成曲線。 為何會發生此情形?
關鍵是原始直線被分成一系列較小的直線。 然後,您可以用不同的方式操作這些較小的直線來形成曲線。
為了協助處理此程式,此範例包含靜態 PathExtensions
類別,其方法 Interpolate
會將直線細分成長度只有一個單位的眾多短線。 此外,類別包含數種方法,可將三種類型的貝塞爾曲線轉換成一系列近似曲線的微小直線。 (參數公式已在文章 中提出貝塞爾曲線的三種類型。此程式稱為 扁平化 曲線:
static class PathExtensions
{
...
static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
{
int count = (int)Math.Max(1, Length(pt0, pt1));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * pt0.X + t * pt1.X;
float y = (1 - t) * pt0.Y + t * pt1.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
3 * t * (1 - t) * (1 - t) * pt1.X +
3 * t * t * (1 - t) * pt2.X +
t * t * t * pt3.X;
float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
3 * t * (1 - t) * (1 - t) * pt1.Y +
3 * t * t * (1 - t) * pt2.Y +
t * t * t * pt3.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
x /= denominator;
y /= denominator;
points[i] = new SKPoint(x, y);
}
return points;
}
static double Length(SKPoint pt0, SKPoint pt1)
{
return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
}
}
所有這些方法都會從擴充方法 CloneWithTransform
參考,也包含在這個類別中,如下所示。 這個方法會藉由列舉路徑命令,並根據數據建構新的路徑來複製路徑。 不過,新路徑只 MoveTo
包含和 LineTo
呼叫。 所有曲線和直線都會縮減為一系列微小的線條。
CloneWithTransform
呼叫 時,您會傳遞至 方法 ,Func<SKPoint, SKPoint>
這是具有SKPaint
傳回SKPoint
值之參數的函式。 每個點都會呼叫此函式來套用自訂演演算法轉換:
static class PathExtensions
{
public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
{
SKPath pathOut = new SKPath();
using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
{
SKPoint[] points = new SKPoint[4];
SKPathVerb pathVerb = SKPathVerb.Move;
SKPoint firstPoint = new SKPoint();
SKPoint lastPoint = new SKPoint();
while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
{
switch (pathVerb)
{
case SKPathVerb.Move:
pathOut.MoveTo(transform(points[0]));
firstPoint = lastPoint = points[0];
break;
case SKPathVerb.Line:
SKPoint[] linePoints = Interpolate(points[0], points[1]);
foreach (SKPoint pt in linePoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[1];
break;
case SKPathVerb.Cubic:
SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
foreach (SKPoint pt in cubicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[3];
break;
case SKPathVerb.Quad:
SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
foreach (SKPoint pt in quadPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Conic:
SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
foreach (SKPoint pt in conicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Close:
SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
foreach (SKPoint pt in closePoints)
{
pathOut.LineTo(transform(pt));
}
firstPoint = lastPoint = new SKPoint(0, 0);
pathOut.Close();
break;
}
}
}
return pathOut;
}
...
}
因為複製的路徑會縮減為微小的直線,因此轉換函式能夠將直線轉換成曲線。
請注意,方法會保留變數中所呼叫 firstPoint
之每個輪廓的第一個點,以及變數 lastPoint
中每個繪圖命令之後的目前位置。 當遇到動詞命令時,這些變數必須建構最後一 Close
行。
GlobularText 範例會使用此擴充方法,在 3D 效果中似乎將文字包裝在半球周圍:
類別建 GlobularTextPage
構函式會執行此轉換。 它會建立SKPaint
文字的物件,然後從 GetTextPath
方法取得 SKPath
物件。 這是傳遞至 CloneWithTransform
擴充方法以及轉換函式的路徑:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
public GlobularTextPage()
{
Title = "Globular Text";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
using (SKPaint textPaint = new SKPaint())
{
textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
textPaint.TextSize = 100;
using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
{
SKRect textPathBounds;
textPath.GetBounds(out textPathBounds);
globePath = textPath.CloneWithTransform((SKPoint pt) =>
{
double longitude = (Math.PI / textPathBounds.Width) *
(pt.X - textPathBounds.Left) - Math.PI / 2;
double latitude = (Math.PI / textPathBounds.Height) *
(pt.Y - textPathBounds.Top) - Math.PI / 2;
longitude *= 0.75;
latitude *= 0.75;
float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
float y = (float)Math.Sin(latitude);
return new SKPoint(x, y);
});
}
}
}
...
}
轉換函式會先計算兩個名為 longitude
的值,範圍 latitude
從文字頂端和左上方的 –π/2 到文字的右下π/2。 這些值的範圍在視覺上並不令人滿意,因此它們會藉由乘以0.75來減少。 (請嘗試沒有這些調整的程序代碼。文字在北極和南極變得過於模糊,在兩側太薄。這些三維球面座標會依標準公式轉換成二維 x
座標 y
。
新路徑會儲存為欄位。 然後處理程式 PaintSurface
只需要置中並調整路徑,才能在畫面上顯示它:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint pathPaint = new SKPaint())
{
pathPaint.Style = SKPaintStyle.Fill;
pathPaint.Color = SKColors.Blue;
pathPaint.StrokeWidth = 3;
pathPaint.IsAntialias = true;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
canvas.DrawPath(globePath, pathPaint);
}
}
}
這是一個非常多才多藝的技術。 如果 Path Effects 文章中所述 的路徑效果 陣列並不完全包含您應該感受到的內容,這是填補空白的方法。