回転変換
SkiaSharp の回転変換を使用して実現できる効果とアニメーションの確認
回転変換を使用すると、SkiaSharp グラフィックス オブジェクトは水平および垂直の軸との位置合わせの制約から解放されます。
グラフィカル オブジェクトをポイント (0, 0) を中心に回転させる場合、SkiaSharp では RotateDegrees
メソッドと RotateRadians
メソッドの両方をサポートします。
public void RotateDegrees (Single degrees)
public Void RotateRadians (Single radians)
360 度の円は 2π ラジアンと同じであるため、この 2 つの単位間では簡単に変換できます。 使いやすい方を使用してください。 .NET Math
クラス内のすべての三角関数では、ラジアンの単位が使用されます。
角度を大きくすると、回転は時計回りになります。 (デカルト座標系での回転は慣習に従って反時計回りですが、時計回りの回転は SkiaSharp のように下に向かうにつれて増加する Y 座標と一致します)。負の角度と、360 度を超える角度を使用できます。
回転の変換数式は、移動や拡大縮小用のものよりも複雑です。 角度 α の変換数式は次のとおりです。
x' = x•cos(α) – y•sin(α)
y` = x•sin(α) + y•cos(α)
Basic Rotate Page では RotateDegrees
メソッドを示します。 BasicRotate.xaml.cs ファイルでは、そのベースラインをページの真ん中に配置した何らかのテキストが表示され、-360 から 360 の範囲の Slider
に基づいてそれを回転させます。 PaintSurface
ハンドラーの関連部分を次に示します。
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextAlign = SKTextAlign.Center,
TextSize = 100
})
{
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}
回転はキャンバスの左上隅を中心とするため、このプログラムの中で設定されたほとんどの角度で、テキストはスクリーンの外側へと回転します。
非常に多くの場合で、次のバージョンの RotateDegrees
および RotateRadians
メソッドを使用して、指定したピボット ポイントを中心に何かを回転させる必要があります。
public void RotateDegrees (Single degrees, Single px, Single py)
public void RotateRadians (Single radians, Single px, Single py)
Centered Rotate Page は、拡張されたバージョンの RotateDegrees
を使用して、回転の中心をテキストの位置決めに使用するものと同じポイントに設定する以外は、Basic Rotate と同じです。
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextAlign = SKTextAlign.Center,
TextSize = 100
})
{
canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
}
これでテキストは、テキストの位置決めに使うポイント (テキストのベースラインの水平方向の真ん中) を中心に回転するようになりました。
Scale
メソッドの中央揃えバージョンと同様に、RotateDegrees
呼び出しの中央揃えバージョンは手っ取り早い方法です。 このメソッドを次に示します。
RotateDegrees (degrees, px, py);
この呼び出しは、次のものと同じです。
canvas.Translate(px, py);
canvas.RotateDegrees(degrees);
canvas.Translate(-px, -py);
Translate
呼び出しと Rotate
呼び出しを組み合わせることができる場合があることがわかります。 たとえば、Centered Rotate Page 内の RotateDegrees
および DrawText
呼び出しを次に示します。
canvas.RotateDegrees((float)rotateSlider.Value, info.Width / 2, info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
RotateDegrees
呼び出しは、2 つの Translate
呼び出しと 1 つの RotateDegrees
(中央揃えなし) 呼び出しに相当します。
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.DrawText(Title, info.Width / 2, info.Height / 2, textPaint);
特定の位置にテキストを表示する DrawText
呼び出しは、その場所での Translate
呼び出しに続けてポイント (0, 0) で DrawText
を呼び出すことに相当します。
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.Translate(-info.Width / 2, -info.Height / 2);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.DrawText(Title, 0, 0, textPaint);
2 つの連続した Translate
呼び出しは、お互いを打ち消し合います。
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.RotateDegrees((float)rotateSlider.Value);
canvas.DrawText(Title, 0, 0, textPaint);
概念的には、2 つの変換はコード内での表示とは逆の順序で適用されます。 DrawText
呼び出しは、キャンバスの左上隅にテキストを表示します。 RotateDegrees
呼び出しは、左上隅を基準にしてそのテキストを回転させます。 次に、 Translate
呼び出しでそのテキストはキャンバスの中央に移動します。
通常、回転と移動を組み合わせる方法はいくつかあります。 Rotated Text Page では次の表示が作成されます。
RotatedTextPage
クラスの PaintSurface
ハンドラーを次に示します。
static readonly string text = " ROTATE";
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint textPaint = new SKPaint
{
Color = SKColors.Black,
TextSize = 72
})
{
float xCenter = info.Width / 2;
float yCenter = info.Height / 2;
SKRect textBounds = new SKRect();
textPaint.MeasureText(text, ref textBounds);
float yText = yCenter - textBounds.Height / 2 - textBounds.Top;
for (int degrees = 0; degrees < 360; degrees += 30)
{
canvas.Save();
canvas.RotateDegrees(degrees, xCenter, yCenter);
canvas.DrawText(text, xCenter, yText, textPaint);
canvas.Restore();
}
}
}
xCenter
と yCenter
の値は、キャンバスの中心を示します。 yText
の値は、そこから少しオフセットされています。 この値は、テキストをページの垂直方向の中央に正確に配置するために必要な Y 座標です。 次に、for
ループはキャンバスの中心に基づいて回転を設定します。 この回転は 30 度ずつ増加します。 テキストは、yText
の値を使用して描画されます。 text
値の中の単語 "ROTATE" の前にある空白の数は、これらの 12 のテキスト文字列間のつながりが 12 角形に見えるように、経験に基づいて決めました。
このコードを簡略化する 1 つの方法は、DrawText
呼び出しの後でループを通るたびに、回転角度を 30 度増やすことです。 これで、Save
および Restore
呼び出しが不要になります。 degrees
変数が for
ブロック本体の中で使用されなくなったことに注意してください。
for (int degrees = 0; degrees < 360; degrees += 30)
{
canvas.DrawText(text, xCenter, yText, textPaint);
canvas.RotateDegrees(30, xCenter, yCenter);
}
ループの前に Translate
を呼び出し、すべてをキャンバスの中央に移動して、単純な形式の RotateDegrees
を使用することもできます。
float yText = -textBounds.Height / 2 - textBounds.Top;
canvas.Translate(xCenter, yCenter);
for (int degrees = 0; degrees < 360; degrees += 30)
{
canvas.DrawText(text, 0, yText, textPaint);
canvas.RotateDegrees(30);
}
変更された yText
の計算には、yCenter
が組み込まれなくなりました。 これで、DrawText
呼び出しによって、テキストはキャンバスの上部で垂直方向に中央揃えされます。
変換は、概念的にはコード内での表示順とは逆に適用されるため、大抵の場合、より多くのグローバル変換から始めて、それに続けてより多くのローカル変換を行うことができます。 多くの場合で、これが回転と移動を組み合わせる最も簡単な方法です。
たとえば、軸を中心に自転する惑星のように、その中心で自転するグラフィカル オブジェクトを描画するとします。 ただし、あたかも惑星が太陽の周りを循環するように、このオブジェクトはスクリーンの中心の周りを循環する必要もあります。
これを行うには、キャンバスの左上隅にオブジェクトを配置し、アニメーションを使用してその隅を中心に回転させます。 次に、そのオブジェクトを軌道半径のように水平方向に移動します。 ここで、同じく原点を中心に 2 つ目のアニメーション回転を適用します。 これにより、このオブジェクトはその隅を中心に循環します。 ここで、キャンバスの中央に移動します。
これらの変換呼び出しを逆の順序で含む PaintSurface
ハンドラーを次に示します。
float revolveDegrees, rotateDegrees;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint fillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Red
})
{
// Translate to center of canvas
canvas.Translate(info.Width / 2, info.Height / 2);
// Rotate around center of canvas
canvas.RotateDegrees(revolveDegrees);
// Translate horizontally
float radius = Math.Min(info.Width, info.Height) / 3;
canvas.Translate(radius, 0);
// Rotate around center of object
canvas.RotateDegrees(rotateDegrees);
// Draw a square
canvas.DrawRect(new SKRect(-50, -50, 50, 50), fillPaint);
}
}
revolveDegrees
および rotateDegrees
フィールドはアニメーション化されます。 このプログラムでは、Xamarin.FormsAnimation
クラスに基づいて、異なるアニメーション手法を使用します (このクラスは、無料の PDF ダウンロード「Creating Mobile Apps with Xamarin.Forms」の中の第 22 章で説明されています)。OnAppearing
のオーバーライドでは、コールバック メソッドを持つ 2 つの Animation
オブジェクトを作成し、それから Commit
を呼び出してアニメーション時間を設定します。
protected override void OnAppearing()
{
base.OnAppearing();
new Animation((value) => revolveDegrees = 360 * (float)value).
Commit(this, "revolveAnimation", length: 10000, repeat: () => true);
new Animation((value) =>
{
rotateDegrees = 360 * (float)value;
canvasView.InvalidateSurface();
}).Commit(this, "rotateAnimation", length: 1000, repeat: () => true);
}
最初の Animation
オブジェクトは、revolveDegrees
を 10 秒間で 0 度から 360 度までアニメーション化します。 2 つ目は、rotateDegrees
を 1 秒ごとに 0 度から 360 度までアニメーション化し、また PaintSurface
ハンドラーへの別の呼び出しを生成するためにサーフェスを無効にします。 OnDisappearing
のオーバーライドでは、これら 2 つのアニメーションが取り消されます。
protected override void OnDisappearing()
{
base.OnDisappearing();
this.AbortAnimation("revolveAnimation");
this.AbortAnimation("rotateAnimation");
}
いわゆる Ugly Analog Clock プログラム (より魅力的なアナログ時計が後の記事の中で説明されるため) は、回転を使用して時計の分と時間のマークを描画し、針を回転させます。 このプログラムでは、半径が 100 でポイント (0, 0) を中心とする円に基づいて、任意の座標系を使用してこの時計を描画します。 移動と拡大縮小を使用し、ページ上の円を拡大して中央揃えします。
Translate
および Scale
呼び出しは、この時計にグローバルに適用されるため、SKPaint
オブジェクトの初期化に続いて最初に呼び出されます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint strokePaint = new SKPaint())
using (SKPaint fillPaint = new SKPaint())
{
strokePaint.Style = SKPaintStyle.Stroke;
strokePaint.Color = SKColors.Black;
strokePaint.StrokeCap = SKStrokeCap.Round;
fillPaint.Style = SKPaintStyle.Fill;
fillPaint.Color = SKColors.Gray;
// Transform for 100-radius circle centered at origin
canvas.Translate(info.Width / 2f, info.Height / 2f);
canvas.Scale(Math.Min(info.Width / 200f, info.Height / 200f));
...
}
}
2 つの異なるサイズの 60 個のマークがあり、時計の周りに丸で描画する必要があります。 DrawCircle
呼び出しは、時計の中心を基準にして、12:00 に対応するポイント (0, –90) に丸を描画します。 RotateDegrees
呼び出しでは、目盛りごとに回転角度が 6 度ずつ増加します。 angle
変数は、描画するのは大きい丸か小さい丸かどうかを判断するためにのみ使用されます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
...
// Hour and minute marks
for (int angle = 0; angle < 360; angle += 6)
{
canvas.DrawCircle(0, -90, angle % 30 == 0 ? 4 : 2, fillPaint);
canvas.RotateDegrees(6);
}
...
}
}
最後に、PaintSurface
ハンドラーは現在の時刻を取得し、時間、分、秒針の回転角度を計算します。 各針は 12:00 の位置に描画され、回転角度はそれを基準にします。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
...
DateTime dateTime = DateTime.Now;
// Hour hand
strokePaint.StrokeWidth = 20;
canvas.Save();
canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
canvas.DrawLine(0, 0, 0, -50, strokePaint);
canvas.Restore();
// Minute hand
strokePaint.StrokeWidth = 10;
canvas.Save();
canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
canvas.DrawLine(0, 0, 0, -70, strokePaint);
canvas.Restore();
// Second hand
strokePaint.StrokeWidth = 2;
canvas.Save();
canvas.RotateDegrees(6 * dateTime.Second);
canvas.DrawLine(0, 10, 0, -80, strokePaint);
canvas.Restore();
}
}
この時計は確かに機能しますが、針はかなり大雑把です。
より魅力的な時計については、「SkiaSharp の SVG パス データ」の記事を参照してください。