SkiaSharp のパス効果
パスのストロークと塗りつぶすために使用できるさまざまなパス効果の学習
パス効果は、クラスによって定義された 8 つの静的作成方法のいずれかを使用して作成される SKPathEffect
クラスのインスタンスです。 その後、SKPathEffect
オブジェクトは、SKPaint
オブジェクトの PathEffect
プロパティに設定され、さまざまな興味深い効果が得られます。たとえば、レプリケートされた小さなパスを使用して行をストロークするなどです。
パス効果を使用すると、次のことができます。
- ドットとダッシュでの線のストローク
- 塗りつぶされたパスでの線のストローク
- 陰影線での領域の塗りつぶし
- 整列表示されたパスを使用した領域の塗りつぶし
- 鋭い角の丸め
- 線と曲線へのランダムな "ジッター" の追加
さらに、2 つ以上のパス効果を組み合わせることができます。
この記事では、StrokeWidth
や PathEffect
などの SKPaint
のプロパティを適用して、あるパスを別のパスに変換するために SKPaint
の GetFillPath
メソッドを使用する方法についても説明します。 これにより、別のパスの枠線であるパスを取得するなど、いくつかの興味深い手法が得られます。 GetFillPath
は、パス効果に関しても役立ちます。
ドットとダッシュ
PathEffect.CreateDash
メソッドの使用方法については、ドットとダッシュの記事で説明しました。 メソッドの最初の引数は、偶数の 2 つ以上の値を含む配列であり、ダッシュの長さとダッシュ間のギャップの長さを交互に使用します。
public static SKPathEffect CreateDash (Single[] intervals, Single phase)
これらの値はストロークの幅に対して相対的ではありません。 たとえば、ストロークの幅が 10 であり、正方形のダッシュと正方形のギャップで構成される線が必要な場合は、intervals
配列を { 10, 10 } に設定します。 phase
引数は、ダッシュ パターン内の線の開始位置を示します。 この例では、線の開始位置を正方形のギャップにする場合は、phase
を 10 に設定します。
ダッシュの終端は、SKPaint
の StrokeCap
プロパティの影響を受けます。 ストローク幅を広くする場合は、このプロパティを 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
オブジェクトに設定されます。
パスからパスへ
SKPaint
の GetFillPath
メソッドは、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
のさまざまなプロパティ (具体的には StrokeWidth
、StrokeCap
、PathEffect
) を ellipsePath
に適用し、結果のパスを newPath
に配置した結果です。 GetFillPath
メソッドは、宛先パスを塗りつぶすかどうかを示すブール値を返します。この例では、戻り値はパスを塗りつぶすための true
です。
newPaint
の Style
設定を 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 つのオプション (Translate
、Rotate
、Morph
) が左から右に表示されます。
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 であり、y は a と等しくなります。 それがケーブルの中心です。 cosine 関数と同様に、cosh は等しいと言われます。つまり、cosh(–x) は cosh(x) と等しく、正または負の引数を増やすと値が増加します。 これらの値は、ケーブルの側面を形成する曲線を表します。
ケーブルが電話のページの寸法に合うように a の適切な値を見つけるのは、直接計算できることではありません。 w と h が四角形の幅と高さである場合、a の最適な値は、次の式を満たすものです。
cosh(w / 2 / a) = 1 + h / a
LinkedChainPage
クラスの次のメソッドは、等号の左右にある 2 つの式を left
と right
として参照することで、その等式を組み込みます。 a の小さい値の場合、left
は right
より大きくなります。a の大きい値の場合、left
は right
未満です。 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.Create1DPath
の phase
引数がアニメーション化されると、バケツは現われてすぐ外に出てしまいます。
このため、プログラムはまず、コンベヤ ベルトの長さである 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
プロパティに基づいて、ストロークまたは塗りつぶしで使用できます。 実行した図がこれです。
この角丸 7 角形は、以前のプログラムのものと同じであることがわかります。 角の半径が、SKPathEffect.CreateCorner
呼び出しで指定された 50 ではなく、本当に 100 であることを確認する必要がある場合は、プログラムの最後のステートメントのコメントを解除し、角に重ね合わせた半径 100 の円を表示できます。
ランダム ジッター
場合によっては、コンピュータ グラフィックスの完璧な直線を求めているのでなく、少しランダムであるのが好ましいことがあります。 その場合は、SKPathEffect.CreateDiscrete
メソッドを試せます。
public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)
このパス効果は、ストロークまたは塗りつぶす場合に使用できます。 線は、segLength
で指定されたおおよその長さの繋がった弧に分割され、異なる方向に延びています。 元の線からの偏差の範囲は、deviation
で指定します。
最後の引数は、効果に使用される擬似ランダム シーケンスを生成するために使用されるシードです。 ジッター効果は、異なるシードのために少し異なって見えます。 引数の既定値は 0 です。これは、プログラムを実行するたびに常に効果は同じであることを意味します。 画面が再描画されるたびに異なるジッターが必要な場合は、シードをたとえば DataTime.Now
値の Millisecond
プロパティに設定できます。
ジッター実験のページでは、四角形をストロークするさまざまな値を試すことができます。
プログラムは簡単です。 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);
}
}
ここでは、横向きモードで実行されています。
パスの枠線
2 つのバージョンがある、SKPaint
の GetFillPath
メソッドの 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
パスをストロークして、パスの枠線を表示することもできます。
タップしてパスの枠線を設定してこれを示します。 SKCanvasView
と TapGestureRecognizer
は、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
ハンドラーは blueFill
と redThickStroke
ペイント オブジェクトを使用して循環パスをレンダリングします。
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);
}
}
}
}
}
次のように円が塗りつぶされ、ストロークされます。
画面をタップすると、outlineThePath
が true
に設定され、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.CreateSum
と SKPathEffect.CreateCompose
です。
public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)
public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)
どちらの方法も、2 つのパス効果を組み合わせて複合パス効果を作成します。 CreateSum
メソッドは、個別に適用される 2 つのパス効果に似たパス効果を作成しますが、CreateCompose
は 1 つのパス効果 (inner
) を適用し、次に outer
を適用します。
SKPaint
の GetFillPath
メソッドが 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);
}
...
}
ここまでで検証したように、陰影線は領域の内部に正確に狭められるわけではありません。この例では、陰影線は常にダッシュ全体を使用して左から始まります。
これで、単純なドットやダッシュから奇妙な組み合わせまで、幅広いパス効果を見てきました。想像力を駆使して何を作成できるかを試してみてください。