次の方法で共有


パス情報と列挙

パスに関する情報を取得し、コンテンツを列挙する

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 で乗算されます。 結果は、そのパスに沿ってテキストを表示するための新しいテキスト サイズです。

[Path Length] ページのトリプル スクリーンショット

ベジエ曲線が長くなったり短くなったりすると、テキスト サイズが変わるのがわかります。

パスを走査する

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 次ベジエ曲線に沿って、一輪車で行ったり来たりしているように見える棒人間をアニメーション化します。

[Unicycle Half-Pipe] ページのトリプル スクリーンショット

ハーフパイプと一輪車の両方の描画に使用される 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.IteratorSKPath.RawIterator があります。 この 2 つのクラスは非常に似ていますが、SKPath.Iterator では、長さ 0 のパス、または長さ 0 に近いパス内の要素を排除できます。 RawIterator は、以下の例で使用されています。

SKPath.RawIterator のオブジェクトを取得するには、SKPathCreateRawIterator メソッドを呼び出します。 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 効果で半球の周囲にテキストをラップするように見えます。

[Globular Text] ページのトリプル スクリーンショット

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

これは非常に汎用性の高い手法です。 パス効果に関する記事で説明されているパス効果の配列に、必要であると感じたものが含まれていない場合は、この方法でそのギャップを埋めることができます。