次の方法で共有


SkiaSharp 線状グラデーション

SKPaint クラスは、単色で線を引いたり、領域を塗りつぶしたりするために使用する Color プロパティを定義します。 または、色が段階的にブレンドされた "グラデーション" で線を引いたり、領域を塗りつぶしたりすることもできます。

線形グラデーションのサンプル

グラデーションの最も基本的な種類は、"線状" グラデーションです。 色のブレンドは、ある点から別の点への線 ("グラデーション線" と呼ばれます) で発生します。 グラデーション線に対して垂直方向の色は同じです。 2 つの静的 SKShader.CreateLinearGradient メソッドのいずれかを使用して線状グラデーションを作成します。 2 つのオーバーロードは、行列変換を一方は含み、もう一方は含まないという点で異なります。

これらのメソッドは、SKPaintShader プロパティに設定した SKShader 型のオブジェクトを返します。 Shader プロパティが null 値以外の場合は、Color プロパティをオーバーライドします。 この SKPaint オブジェクトを使用して引かれた線や塗りつぶされた領域のベースは、単色ではなくグラデーションになります。

Note

DrawBitmap 呼び出しに SKPaint オブジェクトを含めると、Shader プロパティは無視されます。 SKPaintColor プロパティを使用すると、ビットマップを表示するための透明度レベルを設定できますが (「SkiaSharp ビットマップの表示」を参照)、透明度を持つグラデーションでビットマップを表示する Shader プロパティは使用できません。 透明度を持つグラデーションでビットマップを表示するには、その他の手法も使用できます。これらは、SkiaSharp の円形グラデーションおよび SkiaSharp の合成モードとブレンド モードに関する記事で説明されています。

隅から隅へのグラデーション

多くの場合、線状グラデーションは四角形の 1 つの隅から別の隅に向かって延びています。 始点が四角形の左上隅である場合、グラデーションを次に向かって延ばすことができます。

  • 左下隅に向かって垂直方向
  • 右上隅に向かって水平方向
  • 右下隅に向かって対角線方向

対角線方向の線状グラデーションは、サンプルの SkiaSharp シェーダーとその他の効果に関するセクションの最初のページで示されています。 [Corner-to-Corner Gradient] (隅から隅へのグラデーション) ページでは、コンストラクターに SKCanvasView が作成されます。 PaintSurface ハンドラーは、using ステートメントに SKPaint オブジェクトを作成し、キャンバスの中央に 300 ピクセルの四角形を定義します。

public class CornerToCornerGradientPage : ContentPage
{
    ···
    public CornerToCornerGradientPage ()
    {
        Title = "Corner-to-Corner Gradient";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
        ···
    }

    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())
        {
            // Create 300-pixel square centered rectangle
            float x = (info.Width - 300) / 2;
            float y = (info.Height - 300) / 2;
            SKRect rect = new SKRect(x, y, x + 300, y + 300);

            // Create linear gradient from upper-left to lower-right
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Left, rect.Top),
                                new SKPoint(rect.Right, rect.Bottom),
                                new SKColor[] { SKColors.Red, SKColors.Blue },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Repeat);

            // Draw the gradient on the rectangle
            canvas.DrawRect(rect, paint);
            ···
        }
    }
}

SKPaintShader プロパティには、静的 SKShader.CreateLinearGradient メソッドからの SKShader 戻り値が割り当てられます。 5 つの引数は次のとおりです。

  • グラデーションの始点。ここを四角形の左上隅に設定します
  • グラデーションの終点。ここを四角形の右下隅に設定します
  • グラデーションで使用する 2 つ以上の色の配列
  • グラデーション線内の色の相対位置を示す float 値の配列
  • グラデーション線の端部を超えてグラデーションがどのように動作するかを示す SKShaderTileMode 列挙型のメンバー

グラデーション オブジェクトを作成すると、DrawRect メソッドは、シェーダーを含む SKPaint オブジェクトを使用して、300 ピクセルの四角形を描画します。 iOS、Android、ユニバーサル Windows プラットフォーム (UWP) での実行結果を次に示します。

隅から隅へのグラデーション

グラデーション線は、最初の 2 つの引数として指定された 2 点によって定義されます。 これらの点は "キャンバス" に対しては相対的ですが、グラデーションで表示されるグラフィカル オブジェクトに対しては相対的で "ない" ことに注意してください。 色は、グラデーション線に沿って左上の赤から右下の青に徐々に遷移します。 グラデーション線に対して垂直方向は、色が変化しません。

4 つ目の引数として指定された float 値の配列は、色の配列と 1 対 1 で対応しています。 この値は、それぞれの色が現れる、グラデーション線に沿った相対位置を示します。 ここで、0 とは、グラデーション線の始点で Red が現れることを意味し、1 は線の終点で Blue が現れることを意味します。 数値は昇順で、0 から 1 の範囲にある必要があります。 その範囲内にない場合は、その範囲内に収まるよう調整されます。

配列内の 2 つの値は、0 と 1 以外の値に設定できます。 これを試してみます。

new float[] { 0.25f, 0.75f }

これで、グラデーション線の最初の 4 分の 1 全体が純粋な赤になり、最後の 4 分の 1 が純粋な青になりました。 赤と青が混ざるのは、グラデーション線の中央半分に限られます。

一般に、これらの位置の値は 0 から 1 の間に均等に配置する必要があります。 その場合は、4 つ目の引数として nullCreateLinearGradient に指定するだけで実現できます。

このグラデーションは、300 ピクセルの四角形の 2 つの隅の間で定義されますが、四角形を塗りつぶすだけではありません。 [Corner-to-Corner Gradient] (隅から隅へのグラデーション) ページには、ページ上でのタップまたはマウス クリックに対応する追加コードがいくつかあります。 drawBackground フィールドは、タップごとに truefalse に切り替えられます。 値が true の場合、PaintSurface ハンドラーは同じ SKPaint オブジェクトを使用してキャンバス全体を塗りつぶしてから、小さい四角形を示す黒い四角形を描画します。

public class CornerToCornerGradientPage : ContentPage
{
    bool drawBackground;

    public CornerToCornerGradientPage ()
    {
        ···
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            drawBackground ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPaint paint = new SKPaint())
        {
            ···
            if (drawBackground)
            {
                // Draw the gradient on the whole canvas
                canvas.DrawRect(info.Rect, paint);

                // Outline the smaller rectangle
                paint.Shader = null;
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                canvas.DrawRect(rect, paint);
            }
        }
    }
}

画面をタップした後の表示を次に示します。

隅から隅へのグラデーション フル

グラデーションは、グラデーション線を定義する点を超えて同じパターンで繰り返されます。 この繰り返しは、CreateLinearGradient に対する最後の引数が SKShaderTileMode.Repeat であるため発生します。 (その他のオプションについても近いうちに紹介します)。

また、グラデーション線を指定するために使用する点は一意ではありません。 グラデーション線に対して垂直方向は同じ色となるので、同じ効果に対して指定できるグラデーション線の数は無限にあります。 たとえば、水平方向のグラデーションで四角形を塗りつぶす場合、左上隅と右上隅、左下隅と右下隅、またはそれらの線に対して同一平面で平行な任意の 2 点を指定できます。

対話的に試す

[Interactive Linear Gradient] (対話的線状グラデーション) ページを使用して、線状グラデーションを対話的に試すことができます。 このページでは、「円弧を描画する 3 つの方法」で紹介した InteractivePage クラスを使用します。InteractivePageTouchEffect イベントを処理して、指またはマウスで移動できる TouchPoint オブジェクトのコレクションを保持します。

XAML ファイルは、TouchEffectSKCanvasView の親にアタッチし、SKShaderTileMode 列挙型の 3 つのメンバーの 1 つを選択できる Picker も含んでいます。

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
                       xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Effects.InteractiveLinearGradientPage"
                       Title="Interactive Linear Gradient">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid BackgroundColor="White"
              Grid.Row="0">
            <skiaforms:SKCanvasView x:Name="canvasView"
                                    PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

        <Picker x:Name="tileModePicker"
                Grid.Row="1"
                Title="Shader Tile Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKShaderTileMode}">
                    <x:Static Member="skia:SKShaderTileMode.Clamp" />
                    <x:Static Member="skia:SKShaderTileMode.Repeat" />
                    <x:Static Member="skia:SKShaderTileMode.Mirror" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</local:InteractivePage>

分離コード ファイルのコンストラクターは、線状グラデーションの始点と終点に対して 2 つの TouchPoint オブジェクトを作成します。 PaintSurface ハンドラーは、3 色の配列 (赤、緑、青へのグラデーション) を定義し、Picker から現在の SKShaderTileMode を取得します。

public partial class InteractiveLinearGradientPage : InteractivePage
{
    public InteractiveLinearGradientPage ()
    {
        InitializeComponent ();

        touchPoints = new TouchPoint[2];

        for (int i = 0; i < 2; i++)
        {
            touchPoints[i] = new TouchPoint
            {
                Center = new SKPoint(100 + i * 200, 100 + i * 200)
            };
        }

        InitializeComponent();
        baseCanvasView = canvasView;
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
        SKShaderTileMode tileMode =
            (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
                                        0 : tileModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateLinearGradient(touchPoints[0].Center,
                                                         touchPoints[1].Center,
                                                         colors,
                                                         null,
                                                         tileMode);
            canvas.DrawRect(info.Rect, paint);
        }
        ···
    }
}

PaintSurface ハンドラーは、そのすべての情報から SKShader オブジェクトを作成し、それを使用してキャンバス全体に色を付けます。 float 値の配列は null に設定されます。 それ以外の場合、3 色を均等に配置するには、そのパラメーターを 0、0.5、1 の値で配列に設定します。

PaintSurface ハンドラーの大部分は、複数のオブジェクト (円の輪郭との接点、グラデーション線、接点でグラデーション線に垂直な線) を表示することに使用されます。

public partial class InteractiveLinearGradientPage : InteractivePage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // Display the touch points here rather than by TouchPoint
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 3;

            foreach (TouchPoint touchPoint in touchPoints)
            {
                canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
            }

            // Draw gradient line connecting touchpoints
            canvas.DrawLine(touchPoints[0].Center, touchPoints[1].Center, paint);

            // Draw lines perpendicular to the gradient line
            SKPoint vector = touchPoints[1].Center - touchPoints[0].Center;
            float length = (float)Math.Sqrt(Math.Pow(vector.X, 2) +
                                            Math.Pow(vector.Y, 2));
            vector.X /= length;
            vector.Y /= length;
            SKPoint rotate90 = new SKPoint(-vector.Y, vector.X);
            rotate90.X *= 200;
            rotate90.Y *= 200;

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center - rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center - rotate90,
                            paint);
        }
    }
}

2 つの接点を結ぶグラデーション線は簡単に描画できますが、垂直線にはさらなる作業が必要になります。 グラデーション線はベクトルに変換され、1 単位の長さに正規化された後、90 度回転されます。 その後、そのベクターには 200 ピクセルの長さが指定されます。 これは、グラデーション線に対して垂直になるように接点から延びる 4 本の線を描画するために使用されます。

垂直線は、グラデーションの始点と終点と一致します。 これらの線を超えた部分の状態は、SKShaderTileMode 列挙型の設定によって異なります。

対話型線形グラデーション

3 つのスクリーンショットは、SKShaderTileMode の 3 つの異なる値の結果を示しています。 iOS のスクリーンショットは、SKShaderTileMode.Clamp を示しており、これではグラデーションの境界の色がそのまま拡張されます。 Android のスクリーンショットでは、SKShaderTileMode.Repeat オプションでグラデーション パターンがどのように繰り返されるかを示しています。 UWP スクリーンショットの SKShaderTileMode.Mirror オプションでもパターンが繰り返されていますが、パターンは毎回反転され、色の途切れがありません。

グラデーション上のグラデーション

SKShader クラスでは、Dispose を除き、パブリック プロパティやメソッドは定義されません。 そのため、静的メソッドによって作成された SKShader オブジェクトは変更できません。 2 つの異なるオブジェクトに同じグラデーションを使用する場合でも、グラデーションを少し変える必要がある可能性があります。 そのためには、新しい SKShader オブジェクトを作成する必要があります。

[Gradient Text] (グラデーション テキスト) ページには、どちらも同様のグラデーションで色付けされたテキストと背景が表示されます。

グラデーション テキスト

このグラデーションの唯一の違いは、始点と終点です。 テキストの表示に使用されているグラデーションは、テキストに外接する四角形の隅の 2 点に基づいています。 背景の 2 点は、キャンバス全体に基づいています。 のコードを次に示します。

public class GradientTextPage : ContentPage
{
    const string TEXT = "GRADIENT";

    public GradientTextPage ()
    {
        Title = "Gradient Text";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    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())
        {
            // Create gradient for background
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                new SKPoint(info.Width, info.Height),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw background
            canvas.DrawRect(info.Rect, paint);

            // Set TextSize to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.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;

            // Shift textBounds by that amount
            textBounds.Offset(xText, yText);

            // Create gradient for text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(textBounds.Left, textBounds.Top),
                                new SKPoint(textBounds.Right, textBounds.Bottom),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

まず、SKPaint オブジェクトの Shader プロパティが、背景を塗りつぶすグラデーションを表示するために設定されます。 グラデーション点は、キャンバスの左上隅と右下隅に設定されています。

このコードでは、キャンバスの幅の 90% にテキストが表示されるように、SKPaint オブジェクトの TextSize プロパティを設定します。 テキスト境界は、DrawText メソッドに渡す xTextyText の値を計算して、テキストを中央に配置するために使用されます。

ただし、2 つ目の CreateLinearGradient 呼び出しのグラデーション点は、表示されるときに、キャンバスに対して相対的なテキストの左上隅と右下隅を参照する必要があります。 これは、同じ xTextyText の値で textBounds 四角形をシフトすることによって実現されます。

textBounds.Offset(xText, yText);

これで、四角形の左上隅と右下隅を使用して、グラデーションの始点と終点を設定できるようになりました。

グラデーションのアニメーション化

グラデーションをアニメーション化するには、いくつかの方法があります。 1 つの方法は、始点と終点をアニメーション化することです。 [Gradient Animation] (グラデーション アニメーション) ページでは、キャンバスの中央に配置された円上を 2 つの点が動き回ります。 この円の半径は、キャンバスの幅または高さの半分のいずれか小さい方です。 この円上の始点と終点は互いに反対側にあり、グラデーションは Mirror タイル モードで白から黒に変化しています。

グラデーション アニメーション

コンストラクターによって SKCanvasView が作成されます。 OnAppearing および OnDisappearing メソッドは、アニメーション ロジックを処理します。

public class GradientAnimationPage : ContentPage
{
    SKCanvasView canvasView;
    bool isAnimating;
    double angle;
    Stopwatch stopwatch = new Stopwatch();

    public GradientAnimationPage()
    {
        Title = "Gradient Animation";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 3000;
        angle = 2 * Math.PI * (stopwatch.ElapsedMilliseconds % duration) / duration;
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

OnTimerTick メソッドは、3 秒ごとに 0 から 2π までアニメーション化される angle 値を計算します。

2 つのグラデーション点を計算する 1 つの方法を次に示します。 vector という名前の SKPoint 値は、キャンバスの中心から円の半径上の点まで拡張するように計算されます。 このベクトルの方向は、角度の正弦値と余弦値に基づいています。 次に、反対側の 2 つのグラデーション点を計算します。1 つの点は中心点からそのベクトルを減算することによって計算され、もう 1 つの点は中心点にベクトルを加算することによって計算されます。

public class GradientAnimationPage : ContentPage
{
    ···
    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())
        {
            SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
            int radius = Math.Min(info.Width, info.Height) / 2;
            SKPoint vector = new SKPoint((float)(radius * Math.Cos(angle)),
                                         (float)(radius * Math.Sin(angle)));

            paint.Shader = SKShader.CreateLinearGradient(
                                center - vector,
                                center + vector,
                                new SKColor[] { SKColors.White, SKColors.Black },
                                null,
                                SKShaderTileMode.Mirror);

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

少し異なる方法を用いると、必要なコードが少なくなります。 この方法では、行列変換を含む SKShader.CreateLinearGradient オーバーロード メソッドを最後の引数として使用します。 この方法は、サンプルのバージョンです。

public class GradientAnimationPage : ContentPage
{
    ···
    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.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                info.Width < info.Height ? new SKPoint(info.Width, 0) :
                                                           new SKPoint(0, info.Height),
                                new SKColor[] { SKColors.White, SKColors.Black },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Mirror,
                                SKMatrix.MakeRotation((float)angle, info.Rect.MidX, info.Rect.MidY));

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

キャンバスの幅が高さより小さい場合、2 つのグラデーション点は (0, 0) と (info.Width, 0) に設定されます。 最後の引数として CreateLinearGradient に渡された回転変換により、これらの 2 つの点は画面中央を中心に回転します。

角度が 0 の場合、回転はなく、2 つのグラデーション点はキャンバスの左上隅と右上隅となることに注意してください。 これらの点は、前の CreateLinearGradient 呼び出しで示したように、計算されたグラデーション点とは異なります。 しかし、これらの点は、キャンバスの中心を二等分する水平グラデーション線と "平行" であるため、結果として同じグラデーションになります。

虹のグラデーション

[Rainbow Gradient] (虹のグラデーション) ページでは、キャンバスの左上隅から右下隅に虹が描画されます。 しかし、この虹のグラデーションは本物の虹のようなものではありません。 曲線ではなく直線で、0 から 360 までの色相値を繰り返すことによって決定される 8 つの HSL (色相-彩度-明るさ) 色に基づいています。

SKColor[] colors = new SKColor[8];

for (int i = 0; i < colors.Length; i++)
{
    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
}

このコードは、次に示す PaintSurface ハンドラーの一部です。 このハンドラーは、まず、キャンバスの左上隅から右下隅まで延びる 6 辺の多隅形を定義するパスを作成します。

public class RainbowGradientPage : ContentPage
{
    public RainbowGradientPage ()
    {
        Title = "Rainbow Gradient";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    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())
        {
            float rainbowWidth = Math.Min(info.Width, info.Height) / 2f;

            // Create path from upper-left to lower-right corner
            path.MoveTo(0, 0);
            path.LineTo(rainbowWidth / 2, 0);
            path.LineTo(info.Width, info.Height - rainbowWidth / 2);
            path.LineTo(info.Width, info.Height);
            path.LineTo(info.Width - rainbowWidth / 2, info.Height);
            path.LineTo(0, rainbowWidth / 2);
            path.Close();

            using (SKPaint paint = new SKPaint())
            {
                SKColor[] colors = new SKColor[8];

                for (int i = 0; i < colors.Length; i++)
                {
                    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
                }

                paint.Shader = SKShader.CreateLinearGradient(
                                    new SKPoint(0, rainbowWidth / 2),
                                    new SKPoint(rainbowWidth / 2, 0),
                                    colors,
                                    null,
                                    SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

CreateLinearGradient メソッドの 2 つのグラデーション点は、このパスを定義する 2 点 (両方とも左上隅近くにあります) に基づいています。 1 つ目はキャンバスの上端にあり、2 つ目はキャンバスの左端にあります。 結果は次のとおりです。

虹のグラデーションの問題

これは興味深い画像ですが、意図したものとは少し異なります。 問題は、線状グラデーションを作成するときに、グラデーション線に対して垂直方向は色が変わらないということです。 グラデーション線は、図形が上端と左端に接する点に基づいており、その線は、一般に右下隅まで延びる図形の辺に対して垂直ではありません。 この方法は、キャンバスが正方形の場合にのみうまく機能します。

適切な虹のグラデーションを作成するには、グラデーション線が虹の端に対して垂直である必要があります。 これはより計算は複雑になります。 図の長辺と平行なベクトルを定義する必要があります。 そのベクトルを、その辺に垂直になるように 90 度回転します。 次に、図の幅まで延ばすために rainbowWidth を乗算します。 2 つのグラデーション点は、図の辺上の点を基に、その点にベクトルを加えて計算されます。 サンプルの[Rainbow Gradient] (虹のグラデーション) ページに表示されるコードを次に示します。

public class RainbowGradientPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPath path = new SKPath())
        {
            ···
            using (SKPaint paint = new SKPaint())
            {
                ···
                // Vector on lower-left edge, from top to bottom
                SKPoint edgeVector = new SKPoint(info.Width - rainbowWidth / 2, info.Height) -
                                     new SKPoint(0, rainbowWidth / 2);

                // Rotate 90 degrees counter-clockwise:
                SKPoint gradientVector = new SKPoint(edgeVector.Y, -edgeVector.X);

                // Normalize
                float length = (float)Math.Sqrt(Math.Pow(gradientVector.X, 2) +
                                                Math.Pow(gradientVector.Y, 2));
                gradientVector.X /= length;
                gradientVector.Y /= length;

                // Make it the width of the rainbow
                gradientVector.X *= rainbowWidth;
                gradientVector.Y *= rainbowWidth;

                // Calculate the two points
                SKPoint point1 = new SKPoint(0, rainbowWidth / 2);
                SKPoint point2 = point1 + gradientVector;

                paint.Shader = SKShader.CreateLinearGradient(point1,
                                                             point2,
                                                             colors,
                                                             null,
                                                             SKShaderTileMode.Repeat);

                canvas.DrawPath(path, paint);
            }
        }
    }
}

これで、図が虹色になりました。

虹のグラデーション

無限色

[Infinity Colors] (無限色) ページでも虹のグラデーションが使用されます。 このページでは、「3 つの種類のベジエ曲線」で説明されているパス オブジェクトを使用して無限大記号を描画します。 その後、画像はアニメーション化された虹のグラデーションで色付けされ、画像全体が連続的にスイープされます。

コンストラクターは、無限大記号を描く SKPath オブジェクトを作成します。 パスが作成されると、コンストラクターはパスの四角形の境界を取得することもできます。 次に、gradientCycleLength という名前の値を計算します。 グラデーションが pathBounds 四角形の左上隅と右下隅に基づいている場合、この gradientCycleLength 値はグラデーション パターンの水平方向の幅の合計となります。

public class InfinityColorsPage : ContentPage
{
    ···
    SKCanvasView canvasView;

    // Path information
    SKPath infinityPath;
    SKRect pathBounds;
    float gradientCycleLength;

    // Gradient information
    SKColor[] colors = new SKColor[8];
    ···

    public InfinityColorsPage ()
    {
        Title = "Infinity Colors";

        // Create path for infinity sign
        infinityPath = new SKPath();
        infinityPath.MoveTo(0, 0);                                  // Center
        infinityPath.CubicTo(  50,  -50,   95, -100,  150, -100);   // To top of right loop
        infinityPath.CubicTo( 205, -100,  250,  -55,  250,    0);   // To far right of right loop
        infinityPath.CubicTo( 250,   55,  205,  100,  150,  100);   // To bottom of right loop
        infinityPath.CubicTo(  95,  100,   50,   50,    0,    0);   // Back to center  
        infinityPath.CubicTo( -50,  -50,  -95, -100, -150, -100);   // To top of left loop
        infinityPath.CubicTo(-205, -100, -250,  -55, -250,    0);   // To far left of left loop
        infinityPath.CubicTo(-250,   55, -205,  100, -150,  100);   // To bottom of left loop
        infinityPath.CubicTo( -95,  100, - 50,   50,    0,    0);   // Back to center
        infinityPath.Close();

        // Calculate path information
        pathBounds = infinityPath.Bounds;
        gradientCycleLength = pathBounds.Width +
            pathBounds.Height * pathBounds.Height / pathBounds.Width;

        // Create SKColor array for gradient
        for (int i = 0; i < colors.Length; i++)
        {
            colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
        }

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ···
}

コンストラクターは、虹と colors 配列と SKCanvasView オブジェクトも作成します。

OnAppearing および OnDisappearing メソッドのオーバーライドは、アニメーションのオーバーヘッドを実行します。 OnTimerTick メソッドは、offset フィールドを 0 から gradientCycleLength に 2 秒ごとにアニメーション化します。

public class InfinityColorsPage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    float offset;
    Stopwatch stopwatch = new Stopwatch();
    ···

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 2;     // seconds
        double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
        offset = (float)(gradientCycleLength * progress);
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

最後に、PaintSurface ハンドラーが無限大記号をレンダリングします。 パスには中心点 (0, 0) を囲む負の座標と正の座標が含まれているため、キャンバス上の Translate 変換を使用して中心にシフトします。 移動変換の後には、キャンバスの幅と高さの 95% 以内に収まる範囲で無限大記号を可能な限り大きくする倍率を適用する Scale 変換が続きます。

パスの外接する四角形の幅と高さに STROKE_WIDTH 定数が追加されることに注意してください。 パスの線はこの幅で引かれるため、レンダリングされる無限大記号のサイズは、4 辺すべてでその幅の半分だけ大きくなります。

public class InfinityColorsPage : ContentPage
{
    const int STROKE_WIDTH = 50;
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Set transforms to shift path to center and scale to canvas size
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(0.95f *
            Math.Min(info.Width / (pathBounds.Width + STROKE_WIDTH),
                     info.Height / (pathBounds.Height + STROKE_WIDTH)));

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = STROKE_WIDTH;
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(pathBounds.Left, pathBounds.Top),
                                new SKPoint(pathBounds.Right, pathBounds.Bottom),
                                colors,
                                null,
                                SKShaderTileMode.Repeat,
                                SKMatrix.MakeTranslation(offset, 0));

            canvas.DrawPath(infinityPath, paint);
        }
    }
}

SKShader.CreateLinearGradient の最初の 2 つの引数として渡された点をご確認ください。 これらの点は、元のパスの外接する四角形に基づいています。 最初の点は (–250, –100) で、2 つ目は (250, 100) です。 SkiaSharp の内部では、これらの点は現在のキャンバス変換の対象になるため、表示される無限大記号に正しく配置されます。

CreateLinearGradient に対して最後の引数を指定しないと、無限大記号の左上から右下に延びる虹のグラデーションが表示されます。 (実際には、グラデーションは外接する四角形の左上隅から右下隅まで拡張されます。レンダリングされた無限大記号は、外接する四角形よりも、すべての辺の STROKE_WIDTH 値の半分だけ大きくなります。グラデーションは始点と終点の両方で赤であり、SKShaderTileMode.Repeat で作成されているため、違いは目立ちません)。

CreateLinearGradient に対するこの最後の引数を指定すると、グラデーション パターンが画像全体を連続的にスイープします。

無限色

透明度とグラデーション

グラデーションに使用する色には、透明度を組み込むことができます。 グラデーションをある色から別の色にフェードさせる代わりに、色から透明にフェードさせることができます。

この手法で、興味深いエフェクトを可能にします。 以前から使用されていた例の 1 つである、反射するグラフィカル オブジェクトを示します。

反射グラデーション

逆さまになっているテキストは、上部に透明度が 50% のグラデーションが適用され、下部は完全に透明になっています。 これらの透明度レベルは、0x80 と 0 のアルファ値に関連付けられています。

[Reflection Gradient] (リフレクション グラデーション) ページの PaintSurface ハンドラーは、テキスト サイズをキャンバスの幅の 90% にスケーリングします。 次に、ページの水平方向の中央で、垂直方向の中央に対応するベースライン上にテキストを配置するために、xText および yText の値を計算します。

public class ReflectionGradientPage : ContentPage
{
    const string TEXT = "Reflection";

    public ReflectionGradientPage ()
    {
        Title = "Reflection Gradient";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    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())
        {
            // Set text color to blue
            paint.Color = SKColors.Blue;

            // Set text size to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to position text above center
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2;

            // Draw unreflected text
            canvas.DrawText(TEXT, xText, yText, paint);

            // Shift textBounds to match displayed text
            textBounds.Offset(xText, yText);

            // Use those offsets to create a gradient for the reflected text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, textBounds.Top),
                                new SKPoint(0, textBounds.Bottom),
                                new SKColor[] { paint.Color.WithAlpha(0),
                                                paint.Color.WithAlpha(0x80) },
                                null,
                                SKShaderTileMode.Clamp);

            // Scale the canvas to flip upside-down around the vertical center
            canvas.Scale(1, -1, 0, yText);

            // Draw reflected text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

これらの xText および yText の値は、PaintSurface ハンドラーの下部にある DrawText 呼び出しに、反射されたテキストを表示するために使用されるものと同じ値です。 しかし、そのコードの直前に SKCanvasScale メソッドに対する呼び出しが表示されます。 この Scale メソッドでは、水平方向に 1 (変化なし)、垂直方向に –1 だけスケーリングされ、すべてが逆さまに反転されます。 回転の中心は (0, yText) 点に設定されます。yText はキャンバスの垂直方向の中心であり、もともとは info.Height を 2 で割って計算されます。

Skia では、キャンバス変換の前にグラデーションを使用してグラフィカル オブジェクトに色を付けることに注意してください。 反射されていないテキストが描画されると、textBounds 四角形がシフトされ、表示されたテキストに対応します。

textBounds.Offset(xText, yText);

CreateLinearGradient 呼び出しでは、その四角形の上部から下部へのグラデーションが定義されます。 これは、完全に透明な青 (paint.Color.WithAlpha(0)) から透明度が 50% の青 (paint.Color.WithAlpha(0x80)) へのグラデーションです。 キャンバス変換はテキストを逆さまに反転するので、透明度が 50% の青がベースラインから始まり、テキストの上部で透明になります。