次の方法で共有


SkiaSharp のパス効果

パスのストロークと塗りつぶすために使用できるさまざまなパス効果の学習

パス効果は、クラスによって定義された 8 つの静的作成方法のいずれかを使用して作成される SKPathEffect クラスのインスタンスです。 その後、SKPathEffect オブジェクトは、SKPaint オブジェクトの PathEffect プロパティに設定され、さまざまな興味深い効果が得られます。たとえば、レプリケートされた小さなパスを使用して行をストロークするなどです。

リンク チェーンのサンプル

パス効果を使用すると、次のことができます。

  • ドットとダッシュでの線のストローク
  • 塗りつぶされたパスでの線のストローク
  • 陰影線での領域の塗りつぶし
  • 整列表示されたパスを使用した領域の塗りつぶし
  • 鋭い角の丸め
  • 線と曲線へのランダムな "ジッター" の追加

さらに、2 つ以上のパス効果を組み合わせることができます。

この記事では、StrokeWidthPathEffect などの SKPaint のプロパティを適用して、あるパスを別のパスに変換するために SKPaintGetFillPath メソッドを使用する方法についても説明します。 これにより、別のパスの枠線であるパスを取得するなど、いくつかの興味深い手法が得られます。 GetFillPath は、パス効果に関しても役立ちます。

ドットとダッシュ

PathEffect.CreateDash メソッドの使用方法については、ドットとダッシュの記事で説明しました。 メソッドの最初の引数は、偶数の 2 つ以上の値を含む配列であり、ダッシュの長さとダッシュ間のギャップの長さを交互に使用します。

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

これらの値はストロークの幅に対して相対的ではありません。 たとえば、ストロークの幅が 10 であり、正方形のダッシュと正方形のギャップで構成される線が必要な場合は、intervals 配列を { 10, 10 } に設定します。 phase 引数は、ダッシュ パターン内の線の開始位置を示します。 この例では、線の開始位置を正方形のギャップにする場合は、phase を 10 に設定します。

ダッシュの終端は、SKPaintStrokeCap プロパティの影響を受けます。 ストローク幅を広くする場合は、このプロパティを SKStrokeCap.Round に設定して、ダッシュの終端を丸めるのが一般的です。 この場合、intervals 配列内の値に、丸めによって生じる余分な長さは含まれません。 これは、円形のドットがゼロの幅を指定する必要があることを意味します。 ストローク幅が 10 の場合、円形のドットと同じ直径のドットと間に間隔がある線を作成するには、{ 0, 20 } の intervals 配列を使用します。

アニメーション化された点線のテキストのページは、「テキストとグラフィックスの統合」記事で説明されている中抜きの文字列のページに、SKPaint オブジェクトの Style プロパティを SKPaintStyle.Stroke に設定して中抜きの文字列を表示するという点で似ています。 さらに、アニメーション化された点線のテキストは、この枠線に点線の外観を与えるために SKPathEffect.CreateDash を使用します。また、このプログラムは、SKPathEffect.CreateDash メソッドの phase 引数をアニメーション化して、ドットがテキスト文字の周りを移動して見えるようにします。 横向きモードのページを次に示します。

[アニメーション化された点線のテキスト] ページのトリプル スクリーンショット

AnimatedDottedTextPage クラスは、いくつかの定数を定義することから始まり、アニメーションの OnAppearing メソッドと OnDisappearing メソッドもオーバーライドします。

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

PaintSurface ハンドラーは、テキストを表示する SKPaint オブジェクトを作成することから始まります。 TextSize プロパティは、画面の幅に基づいて調整されます。

public class AnimatedDottedTextPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.MeasureText(text, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

メソッドの末尾に向かって、SKPathEffect.CreateDash メソッドは、フィールドとして定義されている dashArray とアニメーション化された phase 値を使用して呼び出されます。 SKPathEffect インスタンスは、テキストを表示する SKPaint オブジェクトの PathEffect プロパティに設定されます。

または、テキストを測定してページ上に中央揃えする前に、SKPathEffect オブジェクトを SKPaint オブジェクトに設定することもできます。 ただし、その場合、アニメーション化されたドットとダッシュのために、レンダリングされたテキストのサイズは多少変化し、テキストが少し上下する傾向があります。 (試してみる)

また、テキスト文字の周りに円を描いてアニメーション化されたドットが移動するにつれ、閉じた各曲線上に、ドットが描画されて消えるように見える特定の地点があることにも気付くでしょう。 ここで、文字の枠線を定義するパスが開始して終了します。 パスの長さがダッシュ パターンの長さの整数倍 (この場合は 20 ピクセル) でない場合、パスの末尾に収まるのは、そのパターンの一部のみになります。

パスの長さに合わせてダッシュ パターンの長さを調整することはできますが、パスの長さを決定する必要があります。これは、「パス情報と列挙」記事で説明されている手法です。

ドット/ダッシュ変形プログラムは、ダッシュ パターン自体をアニメーション化して、ダッシュがドットに分割され、組み合わさってダッシュをもう一度形成して見えるようにします。

[ドット ダッシュ変形] ページのトリプル スクリーンショット

DotDashMorphPage クラスは、前のプログラムと同様に、OnAppearing メソッドと OnDisappearing メソッドをオーバーライドしますが、このクラスは SKPaint オブジェクトをフィールドとして定義します。

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

PaintSurface ハンドラーは、ページのサイズに基づいて楕円パスを作成し、dashArray 変数と phase 変数を設定するコードの長いセクションを実行します。 アニメーション化された変数 t の範囲が 0 から 1 の場合、if ブロックはその時間を 4 分の 1 に分割し、その 4 分の 1 それぞれの tsub も、範囲が 0 から 1 になります。 最後に、プログラムによって SKPathEffect が作成され、描画用の SKPaint オブジェクトに設定されます。

パスからパスへ

SKPaintGetFillPath メソッドは、SKPaint オブジェクトの設定に基づいて、パスを別のパスに変換します。 このしくみを確認するには、前のプログラムの canvas.DrawPath 呼び出しを次のコードに置き換えます。

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

この新しいコードでは、GetFillPath 呼び出しによって ellipsePath (単なる楕円) が newPath に変換され、newPaint と一緒に表示されます。 newPaint オブジェクトは、Style プロパティが GetFillPath からのブール値の戻り値に基づいて設定されることを除き、すべての既定のプロパティ設定で作成されます。

このビジュアルは、ellipsePaint で設定され、newPaint では設定されていない色を除き、同じです。 newPath には、ellipsePath で定義された単純な楕円ではなく、一連のドットとダッシュを定義する多数のパス輪郭が含まれます。 これは、ellipsePaint のさまざまなプロパティ (具体的には StrokeWidthStrokeCapPathEffect) を ellipsePath に適用し、結果のパスを newPath に配置した結果です。 GetFillPath メソッドは、宛先パスを塗りつぶすかどうかを示すブール値を返します。この例では、戻り値はパスを塗りつぶすための true です。

newPaintStyle 設定を SKPaintStyle.Stroke に変更すると、個々のパスの輪郭が 1 ピクセル幅の線で囲まれていることがわかります。

パスを使用したストローク

SKPathEffect.Create1DPath メソッドは概念的には SKPathEffect.CreateDash に似ていますが、ダッシュとギャップのパターンでなく、パスを指定する点が異なります。 このパスは、線または曲線をストロークするために複数回レプリケートされます。

構文は次のとおりです。

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

一般に、Create1DPath に渡すパスは小さく、ポイント (0,0) の中心になります。 advance パラメーターは、パスが線上でレプリケートされるときに、パスの中心間の距離を示します。 通常、この引数は、パスのおおよその幅に設定します。 phase 引数は、ここでは CreateDash メソッドと同じ役割を果たします。

SKPath1DPathEffectStyle には、次の 3 つのメンバーがあります。

  • Translate
  • Rotate
  • Morph

Translate メンバーは、パスが直線または曲線に沿って複製されるのと同じ向きのままになります。 Rotate の場合、パスは曲線への接線を基準に回転されます。 パスの通常の方向は、水平線の向きです。 Morph は、パス自体もストロークされる線の曲率に合わせて曲線を描く点を除き、Rotate と似ています。

1D パス効果のページでは、これらの 3 つのオプションを示します。 OneDimensionalPathEffectPage.xaml ファイルは、列挙体の 3 つのメンバーに対応する 3 つの項目を含むピッカーを定義します。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

OneDimensionalPathEffectPage.xaml.cs 分離コード ファイルは、3 つの SKPathEffect オブジェクトをフィールドとして定義します。 これらはすべて、SKPath.ParseSvgPathData を使用して作成された SKPath オブジェクトで SKPathEffect.Create1DPath を使用して作成されます。 1 つ目は単純なボックス、2 つ目はひし形、3 つ目は四角形です。 これらは、次の 3 つの効果スタイルを示すために使用されます。

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

    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.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

            canvas.DrawPath(path, pathPaint);
        }
    }
}

PaintSurface ハンドラーは、それ自体の周辺をループするベジエ曲線を作成し、ストロークにどの PathEffect を使用するかを決定するためのピッカーにアクセスします。 次の 3 つのオプション (TranslateRotateMorph) が左から右に表示されます。

[1D パス エフェクト] ページのトリプル スクリーンショット

SKPathEffect.Create1DPath メソッドで指定されたパスは常に塗りつぶされます。 DrawPath メソッドで指定されたパスは、SKPaint オブジェクトの PathEffect プロパティが 1D パス効果に設定されている場合、常にストロークされます。 pathPaint オブジェクトには Style 設定がないことに注意してください。これは通常、既定で Fill ですが、パスはこれに関係なくストロークされます。

Translate の例で使用する四角形は 20 ピクセルの正方形であり、advance 引数は 24 に設定されています。 この違いのために、線がほぼ水平または垂直である場合はボックスの間に間隔ができますが、ボックスの対角線が 28.3 ピクセルであるため、線が対角線の場合は、ボックスは少し重なります。

Rotate の例のひし形も幅 20 ピクセルです。 advance は 20 に設定され、ダイヤモンドが線の曲率と共に回転するにつれてポイントが接触し続けます。

Morph の例の四角形の図形は幅が 50 ピクセルであり、advance 設定は 55 であり、四角形の間に小さな間隔が生じ、ベジエ曲線の周囲に曲がっています。

advance 引数がパスのサイズより小さい場合は、レプリケートされたパスが重複する可能性があります。 これにより、いくつかの興味深い効果が生じる可能性があります。 リンク チェーンのページには、繋がったチェーンを模したように見える一連の重複する円が表示されます。これは、ケーブルの特徴的な形で吊り下げられます。

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

近くでよく見ると、それらが実際には円でないことがわかります。 チェーン内の各リンクは 2 つの円弧であり、隣接するリンクと接続して見えるようなサイズと配置になっています。

均一な重量分布のチェーンまたはケーブルは、ケーブルの形で吊り下げられます。 逆のケーブルの形で構築されたアーチは、アーチの重量からの圧力と等しい配分で弧を描きます。 ケーブルは一見単純な数学的描写になるように見えます。

y = a · cosh(x / a)

cosh は双曲線余弦関数です。 x が 0 と等しい場合、cosh は 0 であり、ya と等しくなります。 それがケーブルの中心です。 cosine 関数と同様に、cosh等しいと言われます。つまり、cosh(–x)cosh(x) と等しく、正または負の引数を増やすと値が増加します。 これらの値は、ケーブルの側面を形成する曲線を表します。

ケーブルが電話のページの寸法に合うように a の適切な値を見つけるのは、直接計算できることではありません。 wh が四角形の幅と高さである場合、a の最適な値は、次の式を満たすものです。

cosh(w / 2 / a) = 1 + h / a

LinkedChainPage クラスの次のメソッドは、等号の左右にある 2 つの式を leftright として参照することで、その等式を組み込みます。 a の小さい値の場合、leftright より大きくなります。a の大きい値の場合、leftright 未満です。 while ループは、a の最適な値まで狭められます。

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

リンクの SKPath オブジェクトがクラスのコンストラクターに作成され、結果の SKPathEffect オブジェクトが、フィールドとして格納されている SKPaint オブジェクトの PathEffect プロパティに設定されます。

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

PaintSurface ハンドラーの主なジョブは、ケーブル自体のパスを作成することです。 a の最適な値を決定し、それを optA 変数に格納した後、ウィンドウの上部からのオフセットも計算する必要があります。 次に、ケーブルの SKPoint 値のコレクションを蓄積し、それをパスに変換し、前に作成した SKPaint オブジェクトでパスを描画できます。

public class LinkedChainPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

このプログラムは、中心に (0, 0) ポイントを持つ Create1DPath で使用されるパスを定義します。 これは、パスの (0, 0) ポイントが、装飾されている線または曲線に沿っているため、妥当と思われます。 ただし、一部の特殊効果に非中央 (0, 0) ポイントを使用できます。

コンベヤ ベルトのページは、ウィンドウの寸法に合わせて大きさが変更された湾曲した上部と下部を備えた長いコンベヤ ベルトに似たパスを作成します。 そのパスは、幅 20 ピクセルのシンプルな SKPaint オブジェクトと灰色でストロークされ、小さなバケツを模したパスを参照する SKPathEffect オブジェクトを持つ別の SKPaint オブジェクトで再度ストロークされます。

[コンベヤ ベルト] ページのトリプル スクリーンショット

バケツのパスの (0, 0) ポイントがハンドルであるため、phase 引数がアニメーション化されると、バケツはコンベヤ ベルトの周りを回転するように見えます。底の水をすくって上から外に投げ出しているでしょう。

ConveyorBeltPage クラスは、OnAppearing メソッドと OnDisappearing メソッドのオーバーライドを使用してアニメーションを実装します。 バケツのパスは、ページのコンストラクターで定義されます。

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

バケツを作成するコードは、バケツを少し大きくして横に向ける 2 つの変換で完了します。 これらの変換を適用する方が、前のコードのすべての座標を調整するよりも簡単でした。

PaintSurface ハンドラーは、まず、コンベヤ ベルト自体のパスを定義します。 これは、20 ピクセル幅の濃い灰色の線で描画される 1 対の線と半円のペアです。

public class ConveyorBeltPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

コンベヤ ベルトを描画するためのロジックは、横向きモードでは機能しません。

バケツは、コンベヤ ベルト上で約 200 ピクセル離して配置する必要があります。 ただし、コンベヤ ベルトは、おそらく 200 ピクセルの倍数の長さではありません。つまり、SKPathEffect.Create1DPathphase 引数がアニメーション化されると、バケツは現われてすぐ外に出てしまいます。

このため、プログラムはまず、コンベヤ ベルトの長さである length という名前の値を計算します。 コンベヤ ベルトは直線と半円で構成されているため、これは簡単な計算です。 次に、バケツの数を、length を 200 で除算して計算します。 これは最も近い整数に丸められ、その数値は length に分割されます。 結果は、バケツの整数の間隔になります。 phase 引数は、その 1 つの分割部です。

再度パスからパスへ

コンベヤ ベルト内の DrawSurface ハンドラーの下部で、canvas.DrawPath 呼び出しをコメント アウトし、次のコードに置き換えます。

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

GetFillPath の前の例と同様に、結果は色を除いて同じことがわかります。 GetFillPath を実行すると、newPath オブジェクトにバケツのパスの複数のコピーが格納されます。各コピーは、呼び出し時にアニメーションによって配置されたのと同じ場所に配置されます。

領域の陰影付け

SKPathEffect.Create2DLines メソッドは、領域を平行線で塗りつぶします。これは、陰影線と呼ばれることがよくあります。 メソッドの構文は次のとおりです。

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

width 引数は、陰影線のストローク幅を指定します。 matrix パラメーターは、拡大縮小とオプションの回転の組み合わせです。 拡大縮小係数は、Skia が陰影線の間隔を設定するために使用するピクセル増分を示します。 線間の分離は、拡大縮小係数から width 引数を差し引いた値です。 拡大縮小係数が width 値以下の場合、陰影線の間に空間はなく、領域は塗りつぶされているように見えます。 水平方向と垂直方向の拡大縮小に同じ値を指定します。

既定では、陰影線は水平です。 matrix パラメータに回転が含まれている場合、陰影線は時計回りに回転します。

陰影塗りつぶしのページでは、このパス効果が示されます。 HatchFillPage クラスは、3 つのパス効果をフィールドとして定義します。1 つ目は幅が 3 ピクセルの水平陰影線であり、6 ピクセル離れた位置に配置されていることを示す拡大縮小係数を持ちます。 したがって、線間の間隔は 3 ピクセルです。 2 番目のパス効果は、幅が 24 ピクセル離れた 6 ピクセルの垂直陰影線用です (したがって、分離は 18 ピクセルです)。3 番目のパス効果は、線が 12 ピクセルで幅が 36 ピクセル離れた斜めの陰影線用です。

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

マトリックス Multiply メソッドに注目してください。 水平方向と垂直方向の拡大縮小係数は同じであるため、拡大縮小マトリックスと回転マトリックスを乗算する順序は関係ありません。

PaintSurface ハンドラーは、これら 3 つのパス効果を、3 つの異なる色と fillPaint と組み合わせて使用して、ページに合わせてサイズを変更した角丸四角形を塗りつぶします。 fillPaint に設定された Style プロパティは無視されます。SKPaint オブジェクトに、SKPathEffect.Create2DLine から作成されたパス効果が含まれている場合、領域は次に関係なく塗りつぶされます。

public class HatchFillPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

結果を注意深く見ると、赤と青の陰影線が角丸四角形に合わせて正確に狭められてはいないことがわかります。 (これは明らかに、基になる Skia コードの特性です)。これが不十分な場合は、斜めの陰影線に対する代替方法が緑色で表示されます。角丸四角形がクリッピング パスとして使用され、陰影線がページ全体に描画されます。

PaintSurface ハンドラーは、角丸四角形をストロークするだけの呼び出しで終了するので、赤と青の陰影線との相違を確認できます。

[陰影塗りつぶし] ページのトリプル スクリーンショット

Android 画面は実際にはそうは見えません。スクリーンショットのスケールを変更したことで、細い赤線と細いスペースが、幅が広くなった赤線と幅が広くなったスペースに統合されて見えます。

パスの塗りつぶし

SKPathEffect.Create2DPath を使用すると、水平方向と垂直方向にレプリケートされるパスで領域を塗りつぶせます。事実上、領域が並べて表示されます。

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

SKMatrix 拡大縮小係数は、レプリケートされたパスの水平方向と垂直方向の間隔を示します。 ただし、この matrix 引数を使用してパスを回転することはできません。パスを回転させる場合は、SKPath で定義された Transform メソッドを使用して、パス自体を回転します。

通常、レプリケートされたパスは、塗りつぶされた領域でなく、画面の左端と上端に整列されます。 この動作をオーバーライドするには、0 と拡大縮小係数との間に平行移動係数を指定して、左と上端からの水平方向と垂直方向のオフセットを指定します。

整列表示されたパスの塗りつぶしのページでは、このパス効果が示されます。 領域の並べて表示に使用されるパスは、PathTileFillPage クラスのフィールドとして定義されます。 水平方向と垂直方向の座標の範囲は -40 から 40 です。つまり、このパスは 80 ピクセルの正方形です。

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

PaintSurface ハンドラーでは、SKPathEffect.Create2DPath 呼び出しによって水平方向と垂直方向の間隔が 64 に設定され、80 ピクセルの正方形のタイルが重なります。 幸いなことに、パスはパズル ピースに似ていて、隣接するタイルとうまく混ざり合っています。

[パス タイル塗りつぶし] ページのトリプル スクリーンショット

元のスクリーンショットからの拡大縮小により、特に Android 画面で歪みが発生します。

これらのタイルは常に全体が表示され、切り詰められることはありません。 最初の 2 つのスクリーンショットでは、塗りつぶされている領域が角丸四角形であることすら明らかではありません。 これらのタイルを特定の領域に切り詰める場合は、クリッピング パスを使用します。

SKPaint オブジェクトの Style プロパティを Stroke に設定してみてください。個々のタイルが塗りつぶされずに、枠線が表示されるのがわかります。

SkiaSharp ビットマップのタイル表示」記事に示すように、整列表示したビットマップで領域塗りつぶすこともできます。

角の丸め

円弧を描画する 3 つの方法」の記事に示されている角丸 7 角形プログラムでは、正接円弧を使用して、7 辺の図形の角を曲線化しました。 別の角丸 7 角形のページには、SKPathEffect.CreateCorner メソッドから作成されたパス効果を使用する格段に簡単な方法を示しています。

public static SKPathEffect CreateCorner (Single radius)

1 つの引数に radius という名前を付けますが、目的の角の半径の半分に設定する必要があります。 (これは基になる Skia コードの特性です)。

AnotherRoundedHeptagonPage クラスの PaintSurface ハンドラーを次に示します。

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = 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;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // 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;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

この効果は、SKPaint オブジェクトの Style プロパティに基づいて、ストロークまたは塗りつぶしで使用できます。 実行した図がこれです。

[もう 1 つの丸め七角形] ページのトリプル スクリーンショット

この角丸 7 角形は、以前のプログラムのものと同じであることがわかります。 角の半径が、SKPathEffect.CreateCorner 呼び出しで指定された 50 ではなく、本当に 100 であることを確認する必要がある場合は、プログラムの最後のステートメントのコメントを解除し、角に重ね合わせた半径 100 の円を表示できます。

ランダム ジッター

場合によっては、コンピュータ グラフィックスの完璧な直線を求めているのでなく、少しランダムであるのが好ましいことがあります。 その場合は、SKPathEffect.CreateDiscrete メソッドを試せます。

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

このパス効果は、ストロークまたは塗りつぶす場合に使用できます。 線は、segLength で指定されたおおよその長さの繋がった弧に分割され、異なる方向に延びています。 元の線からの偏差の範囲は、deviation で指定します。

最後の引数は、効果に使用される擬似ランダム シーケンスを生成するために使用されるシードです。 ジッター効果は、異なるシードのために少し異なって見えます。 引数の既定値は 0 です。これは、プログラムを実行するたびに常に効果は同じであることを意味します。 画面が再描画されるたびに異なるジッターが必要な場合は、シードをたとえば DataTime.Now 値の Millisecond プロパティに設定できます。

ジッター実験のページでは、四角形をストロークするさまざまな値を試すことができます。

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

プログラムは簡単です。 JitterExperimentPage.xaml ファイルは、2 つの Slider 要素と 1 つの SKCanvasView をインスタンス化します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

JitterExperimentPage.xaml.cs 分離コード ファイル内の PaintSurface ハンドラーは、Slider 値が変更されるたびに呼び出されます。 2 つの Slider 値を使用して SKPathEffect.CreateDiscrete を呼び出し、それを使用して四角形をストロークします。

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

この効果は、塗りつぶしにも使用できます。その場合、塗りつぶされた領域の枠線は、これらのランダムな偏差の対象となります。 ジッター テキストのページでは、このパス効果を使用してテキストを表示する方法を示します。 JitterTextPage クラスの PaintSurface ハンドラーのコードのほとんどは、テキストのサイズ設定と中央揃えに専念するものです。

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);

        // Calculate offsets to center the text on the screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

ここでは、横向きモードで実行されています。

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

パスの枠線

2 つのバージョンがある、SKPaintGetFillPath メソッドの 2 つの短い例を既に見てきました。

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

最初の 2 つの引数のみが必須です。 このメソッドは、src 引数によって参照されるパスにアクセスし、SKPaint オブジェクトのストローク プロパティ (PathEffect プロパティを含む) に基づいてパス データを変更し、結果を dst パスに書き込みます。 resScale パラメーターを使用すると、より小さな宛先パスを作成する精度を下げることができます。cullRect 引数を使用すると、四角形の外側の輪郭を除去できます。

このメソッドの 1 つの基本的な使用方法には、パスの効果がまったく含まれません。SKPaint オブジェクトの Style プロパティが SKPaintStyle.Stroke に設定されていて、PathEffect が設定されていない場合、GetFillPath は、描画プロパティによってストロークされたかのように、ソース パスの枠線を表すパスを作成します。

たとえば、src パスが半径 500 の単純な円であり、SKPaint オブジェクトがストローク幅 100 を指定している場合、dst パスは、1 つは半径が 450、もう 1 つは半径が 550 の 2 つの同心円になります。 この dst パスの塗りつぶしは、src パスのストロークと同じであるため、このメソッドは GetFillPath と呼ばれます。 ただし、dst パスをストロークして、パスの枠線を表示することもできます。

タップしてパスの枠線を設定してこれを示します。 SKCanvasViewTapGestureRecognizer は、TapToOutlineThePathPage.xaml ファイルでインスタンス化されます。 TapToOutlineThePathPage.xaml.cs 分離コード ファイルでは、3 つの SKPaint オブジェクトをフィールドとして定義します。ストロークのうち 2 つはストローク幅 100 と 20 でストロークし、3 つ目は塗りつぶし用です。

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

画面がタップされていない場合、PaintSurface ハンドラーは blueFillredThickStroke ペイント オブジェクトを使用して循環パスをレンダリングします。

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

次のように円が塗りつぶされ、ストロークされます。

通常の [タップしてパスのアウトラインを表示する] ページのトリプル スクリーンショット

画面をタップすると、outlineThePathtrue に設定され、PaintSurface ハンドラーによって新しい SKPath オブジェクトが作成され、redThickStroke ペイント オブジェクトの GetFillPath への呼び出しの宛先パスとして使用されます。 その後、その宛先パスが塗りつぶされ、redThinStroke でストロークされ、次のようになります。

[タップしてパスのアウトラインを表示する] ページのアウトラインが表示されたトリプル スクリーンショット

2 つの赤い円は、元の円形パスが 2 つの円形の輪郭に変換されたことを明確に示しています。

このメソッドは、SKPathEffect.Create1DPath メソッドに使用するパスを開発する場合に非常に便利です。 これらのメソッドで指定したパスは、パスがレプリケートされるときに常に塗りつぶされます。 パス全体を塗りつぶさない場合、枠線を慎重に定義する必要があります。

たとえば、リンク チェーンのサンプルでは、リンクは 4 つの一連の円弧で定義され、各ペアは 2 つの半径に基づいて、塗りつぶすパスの領域に枠線を描画しました。 LinkedChainPage クラスのコードを置き換えて、これを少し異なる方法で実行できます。

まず、linkRadius 定数を再定義します。

const float linkRadius = 27.5f;
const float linkThickness = 5;

これで、linkPath は、その 1 つの半径に基づき目的の開始角度と進行角度を持つ 2 つの円弧になりました。

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

outlinePath オブジェクトは、strokePaint で指定されたプロパティでストロークされるときに、linkPath の枠線の対象になります。

この手法を使用するもう 1 つの例として、メソッドで使用されるパス用に次に示します。

パス効果の組み合わせ

SKPathEffect の 2 つの最後の静的作成方法は、SKPathEffect.CreateSumSKPathEffect.CreateCompose です。

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

どちらの方法も、2 つのパス効果を組み合わせて複合パス効果を作成します。 CreateSum メソッドは、個別に適用される 2 つのパス効果に似たパス効果を作成しますが、CreateCompose は 1 つのパス効果 (inner) を適用し、次に outer を適用します。

SKPaintGetFillPath メソッドが SKPaint プロパティ (PathEffect を含む) に基づいてパスを別のパスに変換する方法は既に確認しました。そのため、SKPaint オブジェクトが、CreateSum メソッドまたは CreateCompose メソッドで指定された 2 つのパス効果を使用してその処理を 2 回実行する方法は、とても不可解なものにはならないはずです。

1 つのわかりやすい CreateSum の用途は、パスを 1 つのパス効果で塗りつぶし、そのパスを別のパス効果でストロークする SKPaint オブジェクトを定義することです。 これは、フレーム内の猫を示すサンプルです。ここに、スカラップの縁取りがあるフレーム内に猫が整列して表示されます。

[フレーム内の猫] ページのトリプル スクリーンショット

CatsInFramePage クラスは、まず複数のフィールドを定義します。 「SVG パス データ」記事から、PathDataCatPage クラスの最初のフィールドを確認できます。 2 番目のパスは、縁取りのスカラップのパターンの線と円弧に基づいています。

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    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 catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

SKPaint オブジェクトの Style プロパティが Stroke に設定されている場合、SKPathEffect.Create2DPath メソッドで catPath を使用できます。 しかし、catPath がこのプログラムで直接使用されている場合、猫の頭全体が塗りつぶされ、ひげも見えません。 (試してみる)そのパスの枠線を取得し、その枠線を SKPathEffect.Create2DPath メソッドで使用する必要があります。

コンストラクターがこのジョブを実行します。 最初に 2 つの変換を catPath に適用して、(0, 0) ポイントを中心に移動し、サイズを縮小します。 GetFillPath は、outlinedCatPath で輪郭のすべての枠線を取得し、そのオブジェクトは SKPathEffect.Create2DPath 呼び出しで使用されます。 SKMatrix 値の拡大縮小係数は、タイル間に少しバッファーを設けるために、猫の横と縦のサイズよりもわずかに大きくしています。一方で、変換係数は経験的に導き出されたもので、フレームの左上隅に猫全体が表示されるようにしています。

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

コンストラクターはスカラップの縁取りのために SKPathEffect.Create1DPath を呼び出します。 パスの幅は 100 ピクセルですが、フレームの周囲にレプリケートされたパスが重なるように、前進が 75 ピクセルであることに注意してください。 コンストラクターの最後のステートメントは、SKPathEffect.CreateSum を呼び出して 2 つのパス効果を結合し、結果を SKPaint オブジェクトに設定します。

この作業すべてによって、PaintSurface ハンドラーを極めて単純にできます。 四角形を定義し、framePaint を使用して描画するだけで済みます。

public class CatsInFramePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

パス効果の背後にあるアルゴリズムでは、常に、ストロークまたは塗りつぶしに使用されるパス全体が表示されるため、一部のビジュアルが四角形の外側に表示される可能性があります。 DrawRect 呼び出しの前の ClipRect 呼び出しにより、ビジュアルが大幅にクリーンになります。 (クリッピングなしで試してみてください。)

別のパス効果にジッターを追加するには、SKPathEffect.CreateCompose を使用するのが一般的です。 もちろん自分で実験できますが、次に少し異なる例を示します。

破線の陰影線は、破線の陰影線で楕円を塗りつぶします。 DashedHatchLinesPage クラスの作業のほとんどは、フィールド定義で直接実行されます。 これらのフィールドは、ダッシュ効果と陰影効果を定義します。 これらは、SKPaint 定義内の SKPathEffect.CreateCompose 呼び出しで参照されるため、static として定義されます。

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

PaintSurface ハンドラーには、標準のオーバーヘッドに加えて、DrawOval への呼び出しを 1 つだけ含める必要があります。

public class DashedHatchLinesPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

ここまでで検証したように、陰影線は領域の内部に正確に狭められるわけではありません。この例では、陰影線は常にダッシュ全体を使用して左から始まります。

[破線の陰影ライン] ページのトリプル スクリーンショット

これで、単純なドットやダッシュから奇妙な組み合わせまで、幅広いパス効果を見てきました。想像力を駆使して何を作成できるかを試してみてください。