スケール変換
オブジェクトをさまざまなサイズに拡大縮小するための SkiaSharp の拡大縮小変換について
「移動変換」の記事で説明したように、移動変換ではグラフィカル オブジェクトをある場所から別の場所に移動できます。 これに対し、拡大縮小変換はグラフィカル オブジェクトのサイズを変更します。
また、拡大縮小変換では、グラフィックス座標が拡大されるにつれて移動することもよくあります。
以前に、変換係数 dx
および dy
の影響を記述する次の 2 つの変換式について紹介しました。
x' = x + dx
y' = y + dy
拡大縮小係数 sx
および sy
は加法的ではなく乗法的です。
x' = sx · x
y' = sy · y
移動係数の既定値は 0 で、拡大縮小係数の既定値は 1 です。
SKCanvas
クラスでは、4 つの Scale
メソッドが定義されます。 最初の Scale
メソッドは、水平および垂直方向の拡大縮小係数を同じにする場合に適しています。
public void Scale (Single s)
これは等方性拡大縮小と呼ばれ、両方向で同じ拡大縮小を行います。 等方性拡大縮小では、オブジェクトの縦横比が維持されます。
2 つ目の Scale
メソッドでは、水平および垂直方向の拡大縮小に異なる値を指定できます。
public void Scale (Single sx, Single sy)
これにより、異方性拡大縮小がが行われます。
3 つ目の Scale
メソッドでは、2 つの拡大縮小係数を 1 つの SKPoint
値に結合します。
public void Scale (SKPoint size)
4 つ目の Scale
メソッドについては後で説明します。
「基本スケール」ページでは、Scale
メソッドについて説明しています。 BasicScalePage.xaml ファイルには、水平および垂直方向の拡大縮小係数を 0 から 10 の間で選択できる 2 つの Slider
エレメントがあります。 BasicScalePage.xaml.cs 分離コード ファイルでは、これらの値を使用して Scale
を呼び出し、キャンバスの左上隅のテキストに合わせてサイズ調整された、破線でストロークされた角丸四角形を表示します。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.SkyBlue);
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 3,
PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
})
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 50
})
{
canvas.Scale((float)xScaleSlider.Value,
(float)yScaleSlider.Value);
SKRect textBounds = new SKRect();
textPaint.MeasureText(Title, ref textBounds);
float margin = 10;
SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
}
}
拡大縮小係数が SKPaint
の MeasureText
メソッドから返される値に与える影響について疑問に思うかもしれません。 影響はまったくありません。 Scale
は SKCanvas
のメソッドです。 キャンバスにレンダリングするために SKPaint
オブジェクトを使用するまで、このオブジェクトに対する操作には影響しません。
ご覧のとおり、Scale
の呼び出し後に描画されたすべてのものが比例的に増加します。
テキスト、破線の幅、その線内のダッシュの長さ、角の丸み、キャンバスの左端と上端と角丸四角形の間の 10 ピクセルの余白のすべてに同じ縮小変換係数が適用されます。
重要
ユニバーサル Windows プラットフォームでは、異方性拡大縮小が行われたテキストは適切にレンダリングされません。
異方性拡大縮小を使用すると、水平および垂直軸に沿った線のストローク幅が異なるようになります (これは、このページの最初の画像からも明らかです)。拡大縮小係数がストローク幅に影響しないようにするには、ストローク幅を 0 に設定します。これにより、Scale
の設定に関係なく常に幅が 1 ピクセルになります。
拡大縮小は、キャンバスの左上隅を起点にしています。 これはユーザーが期待する動作である場合もあれば、そうでない場合もあります。 テキストと四角形をキャンバス上の別の場所に配置して、キャンバスの中心を起点に拡大縮小する必要があるとします。 その場合、Scale
メソッドの 4 つ目のバージョンを使用できます。このバージョンには、拡大縮小の中心を指定する 2 つの追加パラメーターが含まれています。
public void Scale (Single sx, Single sy, Single px, Single py)
px
および py
パラメーターは拡大縮小の中心と呼ばれることもあるポイントを定義しますが、SkiaSharp ドキュメントではピボット ポイントと呼ばれます。 これは、拡大縮小の影響を受けない、キャンバスの左上隅を起点としたポイントです。 すべての拡大縮小は、その中心を起点にして行われます。
「中央揃えスケール」ページでは、この仕組みについて説明しています。 PaintSurface
ハンドラーは基本スケール プログラムに似ていますが、margin
値がテキストを水平方向に中央揃えするように計算されるため、このプログラムは縦長モードで最適に機能します。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.SkyBlue);
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 3,
PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
})
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 50
})
{
SKRect textBounds = new SKRect();
textPaint.MeasureText(Title, ref textBounds);
float margin = (info.Width - textBounds.Width) / 2;
float sx = (float)xScaleSlider.Value;
float sy = (float)yScaleSlider.Value;
float px = margin + textBounds.Width / 2;
float py = margin + textBounds.Height / 2;
canvas.Scale(sx, sy, px, py);
SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
}
}
角丸四角形の左上隅は、キャンバスの左から margin
ピクセル、上から margin
ピクセル離れた位置に配置されます。 Scale
メソッドの最後の 2 つの引数は、これらの値にテキストの幅と高さを加えた値 (角丸四角形の幅と高さ) に設定されます。 これは、すべての拡大縮小の基準がその四角形の中心であることを意味します。
このプログラムの Slider
要素の範囲は -10 から 10 です。 ご覧のとおり、垂直方向の拡大縮小に負の値を指定すると (中央の Android 画面など)、拡大縮小の中心を通る水平軸を中心にオブジェクトが反転します。 水平方向の拡大縮小に負の値を指定すると (右側の UWP 画面など)、拡大縮小の中心を通る垂直軸を中心にオブジェクトが反転します。
ピボット ポイントを使用した Scale
メソッドのバージョンは、一連の 3 つの Translate
および Scale
呼び出しのショートカットです。 「中央揃えスケール」ページの Scale
メソッドを次に置き換えることで、この仕組みを確認できます。
canvas.Translate(-px, -py);
これらは、ピボット ポイント座標の負の値です。
そして、再びプログラムを実行します。 中心がキャンバスの左上隅になるように四角形とテキストが移動していることがわかります。 これはほとんど見えません。 スライダーは、プログラムが一切拡大縮小を行わなくなったため、もちろん機能しません。
次に、その Translate
呼び出しの前に、(拡大縮小の中心を指定しない) 基本的な Scale
呼び出しを追加します。
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
他のグラフィックス プログラミング システムでこの演習に慣れている場合は、この操作が間違っていると思うかもしれませんが、正しい操作です。 Skia は、連続する変換呼び出しを、慣れ親しんだ方法とは少し異なる方法で処理します。
Scale
と Translate
を連続して呼び出すと、角丸四角形の中心は左上隅のままですが、キャンバスの左上隅 (角丸四角形の中心)を起点として拡大縮小できるようになります。
次に、その Scale
呼び出しの前に、中央揃えの値を指定した別の Translate
呼び出しを追加します。
canvas.Translate(px, py);
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
これにより、拡大縮小された結果が元の位置に戻ります。 これらの 3 つの呼び出しは、次と等価です。
canvas.Scale(sx, sy, px, py);
個々の変換は合算されるため、変換の全体的な数式は次のようになります。
x' = sx · (x – px) + px
y' = sy · (y – py) + py
sx
と sy
の既定値は 1 であることに注意してください。 ピボット ポイント (px, py) がこれらの数式によって変換されないと思い込むのは簡単です。 キャンバスを起点にして同じ場所に残ります。
Translate
と Scale
の呼び出しを組み合わせる際は、順序が重要です。 Translate
が Scale
の後に実行される場合、移動係数は拡大縮小係数によって効果的に拡大縮小されます。 Translate
が Scale
の前に実行される場合、移動係数は拡大縮小されません。 変換行列の対象が導入されると、このプロセスは少し明確になります (ただし、より数学的になります)。
SKPath
クラスは、パス内の座標の範囲を定義する SKRect
を返す読み取り専用 Bounds
プロパティを定義します。 たとえば、以前に作成した 11 芒星のパスから Bounds
プロパティを取得すると、四角形の Left
および Top
プロパティは約 -100、Right
および Bottom
プロパティは約 100、Width
および Height
プロパティは約 200 になります (実際の値のほとんどは少し低くなります。これは、星の点は半径 100 の円によって定義されていますが、水平または垂直軸と平行なのは一番上の点のみであるためです)。
この情報を利用できるかどうかは、パスをキャンバスのサイズに拡大縮小するための拡大縮小および移動係数を求めることができる必要があることを意味します。 「異方性拡大縮小」ページでは、11 芒星でこの動作について説明しています。 異方性拡大縮小とは、水平および垂直方向で不均一であることを意味し、星は元の縦横比を維持しません。 PaintSurface
ハンドラー内の関連コードを次に示します。
SKPath path = HendecagramPage.HendecagramPath;
SKRect pathBounds = path.Bounds;
using (SKPaint fillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Pink
})
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 3,
StrokeJoin = SKStrokeJoin.Round
})
{
canvas.Scale(info.Width / pathBounds.Width,
info.Height / pathBounds.Height);
canvas.Translate(-pathBounds.Left, -pathBounds.Top);
canvas.DrawPath(path, fillPaint);
canvas.DrawPath(path, strokePaint);
}
pathBounds
四角形はこのコードの先頭付近で取得され、その後 Scale
呼び出しでキャンバスの幅と高さとともに使用されます。 その呼び出し自体によって、DrawPath
呼び出しでレンダリングされる際のパスの座標が拡大縮小されますが、星はキャンバスの右上隅を中心として配置されます。 下に移動し、左に移動する必要があります。 これが Translate
呼び出しのジョブです。 pathBounds
のこれらの 2 つのプロパティは約 -100 であるため、移動係数は約 100 になります。 Translate
呼び出しは Scale
呼び出しの後であるため、これらの値は拡大縮小係数によって効果的に拡大縮小されます。これにより、星の中心がキャンバスの中心に移動します。
Scale
と Translate
の呼び出しについて考えられるもう 1 つの方法は、効果を逆の順序で判断することです。Translate
呼び出しによって、パスが移動されて完全に表示されるようになりますが、キャンバスの左上隅に向くようになります。 その後、Scale
メソッドはその星を左上隅を起点にして拡大します。
実際には、星はキャンバスよりも少し大きいようです。 問題はストローク幅です。 SKPath
の Bounds
プロパティは、パスにエンコードされた座標のディメンションを示し、プログラムで拡大縮小に使用されます。 パスが特定のストローク幅でレンダリングされると、レンダリングされたパスはキャンバスよりも大きくなります。
この問題を解決するには、その部分を補正する必要があります。 このプログラムでの簡単な手法の 1 つは、Scale
呼び出しの直前に次のステートメントを追加することです。
pathBounds.Inflate(strokePaint.StrokeWidth / 2,
strokePaint.StrokeWidth / 2);
これにより、pathBounds
四角形の 4 辺すべての長さが 1.5 単位分増加します。 これは、ストローク結合が丸められている場合にのみ有効な解決策です。 マイター結合はさらに長い可能性があり、計算が困難です。
「異方性テキスト」ページで示されているように、テキストにも同様の手法を使用できます。 AnisotropicTextPage
クラスの PaintSurface
ハンドラーの関連部分を次に示します。
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 0.1f,
StrokeJoin = SKStrokeJoin.Round
})
{
SKRect textBounds = new SKRect();
textPaint.MeasureText("HELLO", ref textBounds);
// Inflate bounds by the stroke width
textBounds.Inflate(textPaint.StrokeWidth / 2,
textPaint.StrokeWidth / 2);
canvas.Scale(info.Width / textBounds.Width,
info.Height / textBounds.Height);
canvas.Translate(-textBounds.Left, -textBounds.Top);
canvas.DrawText("HELLO", 0, 0, textPaint);
}
これは同様のロジックであり、テキストは、MeasureText
から返されるテキスト境界の四角形 (実際のテキストより少し大きい) に基づいて、ページのサイズに合わせて拡大されます。
グラフィカル オブジェクトの縦横比を維持する必要がある場合は、等方性拡大縮小を使用します。 「等方性拡大縮小」ページでは、11 芒星でこの動作について説明しています。 概念的には、等方性拡大縮小を使用してページの中央にグラフィカル オブジェクトを表示する手順は次のとおりです。
- グラフィカル オブジェクトの中心を左上隅に移動します。
- グラフィック オブジェクトのディメンションで割った水平および垂直方向のページのディメンションの最小値に基づいて、オブジェクトを拡大縮小します。
- 拡大縮小されたオブジェクトの中心をページの中心に移動します。
IsotropicScalingPage
は星を表示する前に、これらの手順を逆の順序で実行します。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKPath path = HendecagramArrayPage.HendecagramPath;
SKRect pathBounds = path.Bounds;
using (SKPaint fillPaint = new SKPaint())
{
fillPaint.Style = SKPaintStyle.Fill;
float scale = Math.Min(info.Width / pathBounds.Width,
info.Height / pathBounds.Height);
for (int i = 0; i <= 10; i++)
{
fillPaint.Color = new SKColor((byte)(255 * (10 - i) / 10),
0,
(byte)(255 * i / 10));
canvas.Save();
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(scale);
canvas.Translate(-pathBounds.MidX, -pathBounds.MidY);
canvas.DrawPath(path, fillPaint);
canvas.Restore();
scale *= 0.9f;
}
}
}
また、コードでは星がさらに 10 回表示され、そのたびに拡大縮小係数が 10% ずつ減少し、色が赤から青に徐々に変化します。