次の方法で共有


パスおよび領域でのクリッピング

"パスを使用してグラフィックスを特定の領域にクリップし、領域を作成する"

グラフィックスのレンダリングを特定の領域に制限することが必要な場合があります。 これは "クリッピング" と呼ばれます。 鍵穴から見たサルの次の画像など、特殊効果にクリッピングを使用できます。

鍵穴を通して見たサル

"クリッピング領域" は、グラフィックスがレンダリングされる画面の領域です。 クリッピング領域の外側に表示されるものはレンダリングされません。 クリッピング領域は通常、四角形または 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 ハンドラーでは、このパスの境界を取得し、TranslateScale を呼び出して、画面の中央にパスを移動し、画面とほぼ同じ高さにします。

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 の呼び出しで変換をリセットし、ビットマップを描画して画面の全高まで拡張します。 このコードでは、この特定のビットマップが正方形であることが前提となります。 ビットマップは、クリッピング パスによって定義された領域内でのみレンダリングされます。

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

クリッピング パスは、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 つの円の交差部分です。

[Four Circle Intersect Clip] ページのトリプル スクリーンショット

SKClipOperation 列挙体には、次の 2 つのメンバーしかありません。

  • Difference では、指定されたパスまたは四角形が既存のクリッピング領域から削除されます

  • Intersect では、指定されたパスまたは四角形と既存のクリッピング領域を交差させます

FourCircleIntersectClipPage クラスの 4 つの SKClipOperation.Intersect 引数を SKClipOperation.Difference に置き換えた場合、次のように表示されます。

操作を変えた [Four Circle Intersect Clip] ページのトリプル スクリーンショット

クリッピング領域から 4 つの重なる円が削除されました。

[クリップ操作] ページには、円のペアのみを含むこれら 2 つの操作の違いが示されています。 左側の最初の円は、既定の Intersect のクリップ操作でクリッピング領域に追加されますが、右側の 2 番目の円は、テキスト ラベルで示されるクリップ操作でクリッピング領域に追加されます。

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

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 メンバーに基づいてパラメーターとして指定された領域と組み合わされます。 最終的にクリッピングに適した領域を取得したら、SKCanvasClipRegion メソッドを使用して、キャンバスのクリッピング領域として設定できます。

public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)

次のスクリーンショットは、6 つの領域操作に基づくクリッピング領域を示しています。 左の円は Op メソッドが呼び出される領域で、右の円は Op メソッドに渡される領域です。

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

これら 2 つの円を組み合わせる可能性はこれですべてですか? 結果の画像は、DifferenceIntersectReverseDifference 操作で見られる 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 呼び出しでは、比較のために元のパスが青色でストロークされます。

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

この領域は明らかに一連の離散座標です。

クリッピング領域に関連して変換を使用する必要がない場合は、[四つ葉のクローバー] ページに示されているように、領域をクリッピングに使用できます。 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);
                    }
                }
            }
        }
    }
}

それは実際には四つ葉のクローバーのようには見えませんが、クリッピングなしでレンダリングするのは難しい可能性がある画像です。

[Four-Leaf Clover] ページのトリプル スクリーンショット