パスおよび領域でのクリッピング
"パスを使用してグラフィックスを特定の領域にクリップし、領域を作成する"
グラフィックスのレンダリングを特定の領域に制限することが必要な場合があります。 これは "クリッピング" と呼ばれます。 鍵穴から見たサルの次の画像など、特殊効果にクリッピングを使用できます。
"クリッピング領域" は、グラフィックスがレンダリングされる画面の領域です。 クリッピング領域の外側に表示されるものはレンダリングされません。 クリッピング領域は通常、四角形または SKPath
オブジェクトによって定義されますが、SKRegion
オブジェクトを使用してクリッピング領域を定義することもできます。 パスから領域を作成できるため、最初はこれら 2 種類のオブジェクトが関連しているように見えます。 しかし、領域からパスを作成することはできず、内部的には大きく異なります。つまり、パスは一連の線と曲線で構成されますが、領域は一連の水平スキャン ラインによって定義されます。
上の画像は、[鍵穴から見たサル] ページで作成されたものです。 MonkeyThroughKeyholePage
クラスでは、SVG データを使ってパスが定義され、プログラム リソースからビットマップを読み込むためにコンストラクターが使用されます。
public class MonkeyThroughKeyholePage : ContentPage
{
SKBitmap bitmap;
SKPath keyholePath = SKPath.ParseSvgPathData(
"M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130 Z");
public MonkeyThroughKeyholePage()
{
Title = "Monkey through Keyhole";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
bitmap = SKBitmap.Decode(stream);
}
}
...
}
keyholePath
オブジェクトでは鍵穴のアウトラインが記述されますが、座標は完全に任意であり、パス データが考案されたときに便利だったものが反映されています。 このため、PaintSurface
ハンドラーでは、このパスの境界を取得し、Translate
と Scale
を呼び出して、画面の中央にパスを移動し、画面とほぼ同じ高さにします。
public class MonkeyThroughKeyholePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Set transform to center and enlarge clip path to window height
SKRect bounds;
keyholePath.GetTightBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.98f * info.Height / bounds.Height);
canvas.Translate(-bounds.MidX, -bounds.MidY);
// Set the clip path
canvas.ClipPath(keyholePath);
// Reset transforms
canvas.ResetMatrix();
// Display monkey to fill height of window but maintain aspect ratio
canvas.DrawBitmap(bitmap,
new SKRect((info.Width - info.Height) / 2, 0,
(info.Width + info.Height) / 2, info.Height));
}
}
しかし、パスはレンダリングされません。 代わりに、変換後、このステートメントでクリッピング領域を設定するためにパスが使用されます。
canvas.ClipPath(keyholePath);
その後、PaintSurface
ハンドラーでは、ResetMatrix
の呼び出しで変換をリセットし、ビットマップを描画して画面の全高まで拡張します。 このコードでは、この特定のビットマップが正方形であることが前提となります。 ビットマップは、クリッピング パスによって定義された領域内でのみレンダリングされます。
クリッピング パスは、ClipPath
メソッドが呼び出されたときに有効になる変換の対象となり、グラフィカル オブジェクト (ビットマップなど) が表示されるときに有効になる変換の対象にはなりません。 クリッピング パスは、Save
メソッドで保存され、Restore
メソッドで復元されるキャンバス状態の一部です。
クリッピング パスの組み合わせ
厳密に言えば、クリッピング領域は ClipPath
メソッドによって "設定" されません。 代わりに、キャンバスと同じサイズの四角形として始まる既存のクリッピング パスと組み合わされます。 クリッピング領域の四角形の境界は、LocalClipBounds
プロパティまたは DeviceClipBounds
プロパティを使用して取得できます。 LocalClipBounds
プロパティでは、有効になる可能性のある変換を反映する SKRect
値が返されます。 DeviceClipBounds
プロパティでは RectI
値が返されます。 これは整数次元を持つ四角形であり、実際のピクセル次元のクリッピング領域が記述されます。
ClipPath
の呼び出しでは、クリッピング領域と新しい領域を組み合わせることで、クリッピング領域を減らします。 クリッピング領域を四角形と組み合わせる ClipPath
メソッドの完全な構文は次のとおりです。
public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);
既定では、結果のクリッピング領域は、既存のクリッピング領域と、ClipPath
または ClipRect
メソッドで指定された SKPath
または SKRect
の交差部分です。 これは、[4 つの円の交差クリップ] ページで示されています。 FourCircleInteresectClipPage
クラスの PaintSurface
ハンドラーでは、同じ SKPath
オブジェクトを再利用して 4 つの重なる円が作成され、それぞれで ClipPath
を連続して呼び出してクリッピング領域を減らします。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float size = Math.Min(info.Width, info.Height);
float radius = 0.4f * size;
float offset = size / 2 - radius;
// Translate to center
canvas.Translate(info.Width / 2, info.Height / 2);
using (SKPath path = new SKPath())
{
path.AddCircle(-offset, -offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(-offset, offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(offset, -offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
path.Reset();
path.AddCircle(offset, offset, radius);
canvas.ClipPath(path, SKClipOperation.Intersect);
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Fill;
paint.Color = SKColors.Blue;
canvas.DrawPaint(paint);
}
}
}
残っているのは、次の 4 つの円の交差部分です。
SKClipOperation
列挙体には、次の 2 つのメンバーしかありません。
Difference
では、指定されたパスまたは四角形が既存のクリッピング領域から削除されますIntersect
では、指定されたパスまたは四角形と既存のクリッピング領域を交差させます
FourCircleIntersectClipPage
クラスの 4 つの SKClipOperation.Intersect
引数を SKClipOperation.Difference
に置き換えた場合、次のように表示されます。
クリッピング領域から 4 つの重なる円が削除されました。
[クリップ操作] ページには、円のペアのみを含むこれら 2 つの操作の違いが示されています。 左側の最初の円は、既定の Intersect
のクリップ操作でクリッピング領域に追加されますが、右側の 2 番目の円は、テキスト ラベルで示されるクリップ操作でクリッピング領域に追加されます。
ClipOperationsPage
クラスでは、2 つの SKPaint
オブジェクトがフィールドとして定義されてから、画面が 2 つの四角形の領域に分割されます。 これらの領域は、電話が縦モードか横モードかによって異なります。 その後、DisplayClipOp
クラスではテキストが表示され、2 つの円パスを使用して ClipPath
が呼び出され、各クリップ操作が示されます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float x = 0;
float y = 0;
foreach (SKClipOperation clipOp in Enum.GetValues(typeof(SKClipOperation)))
{
// Portrait mode
if (info.Height > info.Width)
{
DisplayClipOp(canvas, new SKRect(x, y, x + info.Width, y + info.Height / 2), clipOp);
y += info.Height / 2;
}
// Landscape mode
else
{
DisplayClipOp(canvas, new SKRect(x, y, x + info.Width / 2, y + info.Height), clipOp);
x += info.Width / 2;
}
}
}
void DisplayClipOp(SKCanvas canvas, SKRect rect, SKClipOperation clipOp)
{
float textSize = textPaint.TextSize;
canvas.DrawText(clipOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
rect.Top += textSize;
float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
float xCenter = rect.MidX;
float yCenter = rect.MidY;
canvas.Save();
using (SKPath path1 = new SKPath())
{
path1.AddCircle(xCenter - radius / 2, yCenter, radius);
canvas.ClipPath(path1);
using (SKPath path2 = new SKPath())
{
path2.AddCircle(xCenter + radius / 2, yCenter, radius);
canvas.ClipPath(path2, clipOp);
canvas.DrawPaint(fillPaint);
}
}
canvas.Restore();
}
DrawPaint
を呼び出すと、通常、キャンバス全体がその SKPaint
オブジェクトで塗りつぶされますが、この場合、メソッドではクリッピング領域内に描画されるだけです。
領域を調べる
SKRegion
オブジェクトの観点からクリッピング領域を定義することもできます。
新しく作成された SKRegion
オブジェクトでは、空の領域が記述されます。 通常、領域で四角形領域が記述されるように、オブジェクトの最初の呼び出しは SetRect
となります。 SetRect
のパラメーターは SKRectI
値です。これは、ピクセル単位で四角形が指定されるため、整数座標を持つ四角形となります。 その後、SKPath
オブジェクトを使用して SetPath
を呼び出すことができます。 これにより、パスの内部と同じ領域が作成されますが、最初の四角形領域にクリップされます。
領域は、次のような Op
メソッド オーバーロードのいずれかを呼び出すことによっても変更できます。
public Boolean Op(SKRegion region, SKRegionOperation op)
SKRegionOperation
列挙体は SKClipOperation
に似ていますが、より多くのメンバーがあります。
Difference
Intersect
Union
XOR
ReverseDifference
Replace
Op
呼び出しを行う領域は、SKRegionOperation
メンバーに基づいてパラメーターとして指定された領域と組み合わされます。 最終的にクリッピングに適した領域を取得したら、SKCanvas
の ClipRegion
メソッドを使用して、キャンバスのクリッピング領域として設定できます。
public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)
次のスクリーンショットは、6 つの領域操作に基づくクリッピング領域を示しています。 左の円は Op
メソッドが呼び出される領域で、右の円は Op
メソッドに渡される領域です。
これら 2 つの円を組み合わせる可能性はこれですべてですか? 結果の画像は、Difference
、Intersect
、ReverseDifference
操作で見られる 3 つのコンポーネントの組み合わせと考えてください。 組み合わせの合計数は、2 の 3 乗、つまり 8 です。 欠落している 2 つは、元の領域 (Op
をまったく呼び出さないことに起因) と完全に空の領域です。
最初にパスを作成し、そのパスから領域を作成し、複数の領域を組み合わせる必要があるため、領域をクリッピングに使用するのは困難です。 [領域操作] ページの全体的な構造は、[クリップ操作] によく似ていますが、RegionOperationsPage
クラスでは画面が 6 つの領域に分割され、このジョブで領域を使用するために必要な追加の作業が示されます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float x = 0;
float y = 0;
float width = info.Height > info.Width ? info.Width / 2 : info.Width / 3;
float height = info.Height > info.Width ? info.Height / 3 : info.Height / 2;
foreach (SKRegionOperation regionOp in Enum.GetValues(typeof(SKRegionOperation)))
{
DisplayClipOp(canvas, new SKRect(x, y, x + width, y + height), regionOp);
if ((x += width) >= info.Width)
{
x = 0;
y += height;
}
}
}
void DisplayClipOp(SKCanvas canvas, SKRect rect, SKRegionOperation regionOp)
{
float textSize = textPaint.TextSize;
canvas.DrawText(regionOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
rect.Top += textSize;
float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
float xCenter = rect.MidX;
float yCenter = rect.MidY;
SKRectI recti = new SKRectI((int)rect.Left, (int)rect.Top,
(int)rect.Right, (int)rect.Bottom);
using (SKRegion wholeRectRegion = new SKRegion())
{
wholeRectRegion.SetRect(recti);
using (SKRegion region1 = new SKRegion(wholeRectRegion))
using (SKRegion region2 = new SKRegion(wholeRectRegion))
{
using (SKPath path1 = new SKPath())
{
path1.AddCircle(xCenter - radius / 2, yCenter, radius);
region1.SetPath(path1);
}
using (SKPath path2 = new SKPath())
{
path2.AddCircle(xCenter + radius / 2, yCenter, radius);
region2.SetPath(path2);
}
region1.Op(region2, regionOp);
canvas.Save();
canvas.ClipRegion(region1);
canvas.DrawPaint(fillPaint);
canvas.Restore();
}
}
}
ClipPath
メソッドと ClipRegion
メソッドの大きな違いを以下に示します。
重要
ClipPath
メソッドとは異なり、ClipRegion
メソッドは変換の影響を受けません。
この違いの根拠を理解するには、領域が何であるかを理解すると役立ちます。 クリップ操作または領域操作を内部で実装する方法について考えた場合、おそらく非常に複雑に思えます。 非常に複雑なパスがいくつか組み合わされている可能性があり、結果のパスのアウトラインはアルゴリズムの悪夢のようです。
このジョブは、昔ながらのブラウン管テレビのような一連の水平スキャン ラインに各パスが縮小された場合に大幅に簡素化されます。 各スキャン ラインは、始点と終点がある単なる水平線です。 たとえば、半径が 10 ピクセルの円を 20 本の水平スキャン ラインに分解でき、それぞれ円の左側から始まり、右側で終わります。 2 つの円を任意の領域操作と組み合わせることは、単に対応するスキャン ラインの各ペアの開始および終了座標を調べるという問題にすぎないため、非常に簡単になります。
つまり、領域とは一連の水平スキャン ラインのことであり、これにより 1 つの領域が定義されます。
ただし、領域が一連のスキャン ラインに縮小された場合、これらのスキャン ラインは特定のピクセル寸法に基づきます。 厳密に言えば、領域はベクター グラフィックス オブジェクトではありません。 その性質は、パスよりも圧縮されたモノクロ ビットマップに近いものとなります。 したがって、領域は忠実性を失わずに拡大縮小または回転することはできません。このため、クリッピング領域に使用しても変換されません。
しかし、描画目的で領域に変換を適用することはできます。 [領域描画] プログラムでは、領域の本質が鮮明に示されています。 RegionPaintPage
クラスでは、半径 10 単位の円の SKPath
に基づいて SKRegion
オブジェクトが作成されます。 その後、変換によってその円が拡大され、ページが塗りつぶされます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int radius = 10;
// Create circular path
using (SKPath circlePath = new SKPath())
{
circlePath.AddCircle(0, 0, radius);
// Create circular region
using (SKRegion circleRegion = new SKRegion())
{
circleRegion.SetRect(new SKRectI(-radius, -radius, radius, radius));
circleRegion.SetPath(circlePath);
// Set transform to move it to center and scale up
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(Math.Min(info.Width / 2, info.Height / 2) / radius);
// Fill region
using (SKPaint fillPaint = new SKPaint())
{
fillPaint.Style = SKPaintStyle.Fill;
fillPaint.Color = SKColors.Orange;
canvas.DrawRegion(circleRegion, fillPaint);
}
// Stroke path for comparison
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Style = SKPaintStyle.Stroke;
strokePaint.Color = SKColors.Blue;
strokePaint.StrokeWidth = 0.1f;
canvas.DrawPath(circlePath, strokePaint);
}
}
}
}
DrawRegion
呼び出しでは、オレンジ色で領域が塗りつぶされますが、DrawPath
呼び出しでは、比較のために元のパスが青色でストロークされます。
この領域は明らかに一連の離散座標です。
クリッピング領域に関連して変換を使用する必要がない場合は、[四つ葉のクローバー] ページに示されているように、領域をクリッピングに使用できます。 FourLeafCloverPage
クラスでは、4 つの円形領域から複合領域が構築され、その複合領域がクリッピング領域として設定され、ページの中央から発せられる一連の 360 直線が描画されます。
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
float xCenter = info.Width / 2;
float yCenter = info.Height / 2;
float radius = 0.24f * Math.Min(info.Width, info.Height);
using (SKRegion wholeScreenRegion = new SKRegion())
{
wholeScreenRegion.SetRect(new SKRectI(0, 0, info.Width, info.Height));
using (SKRegion leftRegion = new SKRegion(wholeScreenRegion))
using (SKRegion rightRegion = new SKRegion(wholeScreenRegion))
using (SKRegion topRegion = new SKRegion(wholeScreenRegion))
using (SKRegion bottomRegion = new SKRegion(wholeScreenRegion))
{
using (SKPath circlePath = new SKPath())
{
// Make basic circle path
circlePath.AddCircle(xCenter, yCenter, radius);
// Left leaf
circlePath.Transform(SKMatrix.MakeTranslation(-radius, 0));
leftRegion.SetPath(circlePath);
// Right leaf
circlePath.Transform(SKMatrix.MakeTranslation(2 * radius, 0));
rightRegion.SetPath(circlePath);
// Make union of right with left
leftRegion.Op(rightRegion, SKRegionOperation.Union);
// Top leaf
circlePath.Transform(SKMatrix.MakeTranslation(-radius, -radius));
topRegion.SetPath(circlePath);
// Combine with bottom leaf
circlePath.Transform(SKMatrix.MakeTranslation(0, 2 * radius));
bottomRegion.SetPath(circlePath);
// Make union of top with bottom
bottomRegion.Op(topRegion, SKRegionOperation.Union);
// Exclusive-OR left and right with top and bottom
leftRegion.Op(bottomRegion, SKRegionOperation.XOR);
// Set that as clip region
canvas.ClipRegion(leftRegion);
// Set transform for drawing lines from center
canvas.Translate(xCenter, yCenter);
// Draw 360 lines
for (double angle = 0; angle < 360; angle++)
{
float x = 2 * radius * (float)Math.Cos(Math.PI * angle / 180);
float y = 2 * radius * (float)Math.Sin(Math.PI * angle / 180);
using (SKPaint strokePaint = new SKPaint())
{
strokePaint.Color = SKColors.Green;
strokePaint.StrokeWidth = 2;
canvas.DrawLine(0, 0, x, y, strokePaint);
}
}
}
}
}
}
それは実際には四つ葉のクローバーのようには見えませんが、クリッピングなしでレンダリングするのは難しい可能性がある画像です。