パス情報と列挙
パスに関する情報を取得し、コンテンツを列挙する
SKPath
クラスは、パスに関する情報を取得できる複数のプロパティとメソッドを定義します。 Bounds
プロパティと TightBounds
プロパティ (および関連するメソッド) は、パスのメトリック ディメンションを取得します。 Contains
メソッドを使用すると、特定のポイントがパス内にあるかどうかを判断できます。
パスを構成するすべての線と曲線の長さの合計を決めておくと便利な場合があります。 この長さの計算はアルゴリズム的に単純なタスクではありません。このため、PathMeasure
という名前のクラス全体がこのタスク専用となります。
また、パスを構成するすべての描画操作とポイントを取得すると便利な場合もあります。 最初は、この機能は不要と思われるかもしれません。パスがプログラムによって作成されている場合、コンテンツはプログラムによって既に認識されているためです。 しかし、パスは、パス効果や、テキスト文字列をパスに変換することによっても作成できることを確認しました。 また、これらのパスを構成するすべての描画操作とポイントを取得することもできます。 アルゴリズム変換をすべてのポイントに適用し、たとえば半球の周囲にテキストをラップする、という可能性もあります。
パスの長さを取得する
パスとテキストに関する記事では、DrawTextOnPath
メソッドを使用して、ベースラインがパスのコースに沿ったテキスト文字列を描画する方法を確認しました。 しかし、テキストのサイズを、パスに正確に合わせて調整したいときはどうすればよいのでしょうか? 円の円周は簡単に計算できるため、円の周りにテキストを描画するのは簡単です。 しかし、楕円の円周やベジエ曲線の長さは、それほど単純ではありません。
SKPathMeasure
クラスは役立ちます。 コンストラクターは SKPath
引数を受け取ります。そして、Length
プロパティによってその長さが明らかになります。
このクラスは、ベジエ曲線ページに基づくパスの長さのサンプルで示されています。 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 分離コード ファイルを使用すると、4 つのタッチ ポイントを移動して、3 次ベジエ曲線のエンド ポイントと制御ポイントを定義できます。 3 つのフィールドでは、テキスト文字列、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
フィールドは、TextSize
設定 10 に基づくテキストの幅です。
PaintSurface
ハンドラーはベジエ曲線を描画し、その長さ全体に合わせて、テキストのサイズを調整します。
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);
}
...
}
新しく作成された SKPathMeasure
オブジェクトの Length
プロパティは、パスの長さを取得します。 パスの長さは、baseTextWidth
値 (テキスト サイズ 10 に基づくテキストの幅) で除算され、基本テキスト サイズ 10 で乗算されます。 結果は、そのパスに沿ってテキストを表示するための新しいテキスト サイズです。
ベジエ曲線が長くなったり短くなったりすると、テキスト サイズが変わるのがわかります。
パスを走査する
SKPathMeasure
は、パスの長さを測定するだけではありません。 SKPathMeasure
オブジェクトは、0 からパスの長さの間の任意の値について、パス上の位置と、そのポイントでのパス曲線への正接を取得できます。 正接は、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
一輪車ハーフパイプ ページでは、3 次ベジエ曲線に沿って、一輪車で行ったり来たりしているように見える棒人間をアニメーション化します。
ハーフパイプと一輪車の両方の描画に使用される 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
ハンドラーは、0 から 1 に移動する t
の値を 5 秒ごとに計算します。 その後 Math.Cos
関数を使用して、それを t
の値 (0 から 1 に移動し、0 に戻る値) に変換します。ここで 0 は左上の最初の一輪車に対応し、1 は右上の一輪車に対応します。 コサイン関数を使用すると、パイプの最上部で速度が最も遅くなり、下部で最速になります。
この値 t
と、GetMatrix
に対する最初の引数のパスの長さを乗算する必要があることに注意してください。 その後、行列は、一輪車パスを描画するために、SKCanvas
オブジェクトに適用されます。
パスを列挙する
SKPath
の 2 つの埋め込みクラスを使用すると、パスのコンテンツを列挙できます。 このようなクラスとしては、SKPath.Iterator
と SKPath.RawIterator
があります。 この 2 つのクラスは非常に似ていますが、SKPath.Iterator
では、長さ 0 のパス、または長さ 0 に近いパス内の要素を排除できます。 RawIterator
は、以下の例で使用されています。
型 SKPath.RawIterator
のオブジェクトを取得するには、SKPath
の CreateRawIterator
メソッドを呼び出します。 Next
メソッドを繰り返し呼び出すことで、パスが列挙されます。 それに、4 つの SKPoint
値の配列を渡します。
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
Next
メソッドは、SKPathVerb
列挙型のメンバーを返します。 これらの値は、パス内の特定の描画コマンドを示します。 配列に挿入される有効なポイントの数は、動詞によって異なります。
- 1 つのポイントを伴う
Move
- 2 つのポイントを伴う
Line
- 4 つのポイントを伴う
Cubic
- 3 つのポイントを伴う
Quad
- 3 つのポイントを伴う
Conic
(また、重みに対してConicWeight
メソッドも呼び出します) - 1 つのポイントを伴う
Close
Done
Done
動詞は、パスの列挙が完了したことを示します。
Arc
動詞がないことに注意してください。 これは、円弧はすべて、パスに追加されるとベジエ曲線に変換されることを示します。
SKPoint
配列の情報の中には冗長なものがあります。 たとえば、Move
動詞の後に Line
動詞が続く場合、Line
に伴う 2 つのポイントのうち最初のポイントは、Move
ポイントと同じです。 実際のところ、この冗長性は非常に便利です。 Cubic
動詞を取得すると、その動詞には、3 次ベジエ曲線を定義する 4 つのポイントすべてが伴っています。 前の動詞によって確立された現在の位置を保持する必要はありません。
ただし、問題となるのは Close
動詞です。 このコマンドは、現在の位置から、Move
コマンドによって先ほど確立された輪郭の始点まで直線を引きます。 理想的には、Close
動詞は 1 つのポイントではなく、この 2 つのポイントを提供する必要があります。 さらに悪いことに、Close
動詞に伴うポイントは常に (0, 0) です。 パスを列挙する場合、高い可能性で Move
ポイントと現在の位置を保持する必要があります。
列挙、フラット化、および変形
アルゴリズム変換をパスに適用して、何らかの形で変形させることが望ましい場合があります。
これらの文字はほとんどが直線で構成されていますが、その直線は明らかに曲線にねじれ込んでいます。 どうすればよいでしょうか。
重要なのは、元の直線が、一連の小さな直線に分割されていることです。 こうした小さな個別の直線をさまざまな方法で操作して、曲線を形成することができます。
このプロセスを支援するために、サンプルには静的な PathExtensions
クラスと Interpolate
メソッドが含まれており、直線を、1 単位の長さしかない多数の短い線に分割します。 さらに、このクラスには、3 種類のベジエ曲線を一連の小さな直線に変換し、その曲線に近づけるメソッドがいくつか含まれています (パラメトリック数式については、3 種類のベジエ曲線に関する記事で紹介しました)。このプロセスは、曲線の "フラット化" と呼ばれます。
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
から参照されます。これはこのクラスに含まれ、以下にも示されます。 このメソッドは、path コマンドを列挙し、データに基づいて新しいパスを構築することで、パスをクローンします。 ただし、新しいパスは、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 から右下π/2 までの範囲) が計算されます。 これらの値の範囲は視覚的に満足のいくものではないので、0.75 を乗算することで縮小します (この調整を行わずにコードを試してみてください。テキストは北極と南極では不明瞭になりすぎます。また、側面では細くなりすぎます)。これらの 3 次元の球座標は、標準の数式によって 2 次元の 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);
}
}
}
これは非常に汎用性の高い手法です。 パス効果に関する記事で説明されているパス効果の配列に、必要であると感じたものが含まれていない場合は、この方法でそのギャップを埋めることができます。