Freigeben über


Der lineare Farbverlauf von SkiaSharp

Die SKPaint Klasse definiert eine Color Eigenschaft, die für Strichlinien oder Füllbereiche mit einer Volltonfarbe verwendet wird. Alternativ können Sie Strichlinien oder Füllbereiche mit Farbverläufen verwenden, die graduelle Farbmischungen sind:

Beispiel für linearen Farbverlauf

Der einfachste Farbverlaufstyp ist ein linearer Farbverlauf. Die Farbmischung erfolgt in einer Linie (als Farbverlaufslinie bezeichnet) von einem Punkt zu einem anderen. Linien, die senkrecht zur Farbverlaufslinie stehen, weisen die gleiche Farbe auf. Sie erstellen einen linearen Farbverlauf mit einer der beiden statischen SKShader.CreateLinearGradient Methoden. Der Unterschied zwischen den beiden Überladungen besteht darin, dass eine Matrixtransformation und die andere nicht enthält.

Diese Methoden geben ein Objekt vom Typ zurück SKShader , das Sie auf die Shader Eigenschaft von SKPaint. Wenn die Shader Eigenschaft ungleich NULL ist, überschreibt sie die Color Eigenschaft. Jede Linie, die mit diesem Objekt gefüllt ist, oder ein beliebiger Bereich, der mit diesem SKPaint Objekt gefüllt ist, basiert auf dem Farbverlauf und nicht auf der Volltonfarbe.

Hinweis

Die Shader Eigenschaft wird ignoriert, wenn Sie ein SKPaint Objekt in einen DrawBitmap Aufruf einschließen. Sie können die Color Eigenschaft SKPaint verwenden, um eine Transparenzstufe zum Anzeigen einer Bitmap festzulegen (wie im Artikel "Anzeigen von SkiaSharp-Bitmaps" beschrieben), aber Sie können die Shader Eigenschaft nicht zum Anzeigen einer Bitmap mit einer Farbverlaufstransparenz verwenden. Weitere Techniken sind zum Anzeigen von Bitmaps mit Farbverlauftransparenzen verfügbar: Diese werden in den Artikeln SkiaSharp-Kreisverläufe und SkiaSharp Compositing- und Blendmodi beschrieben.

Ecken-zu-Eck-Farbverläufe

Häufig erstreckt sich ein linearer Farbverlauf von einer Ecke eines Rechtecks zu einer anderen. Wenn der Startpunkt die obere linke Ecke des Rechtecks ist, kann der Farbverlauf erweitert werden:

  • vertikal zur unteren linken Ecke
  • horizontal zur oberen rechten Ecke
  • diagonal zur unteren rechten Ecke

Der diagonale lineare Farbverlauf wird auf der ersten Seite im Abschnitt "SkiaSharp-Shader und andere Effekte " des Beispiels veranschaulicht. Die Seite "Eck-zu-Ecke-Farbverlauf " erstellt einen SKCanvasView im Konstruktor. Der PaintSurface Handler erstellt ein SKPaint Objekt in einer using Anweisung und definiert dann ein quadratisches Rechteck mit 300 Pixeln zentriert im Zeichenbereich:

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);
            ···
        }
    }
}

Der Shader Eigenschaft wird SKPaint der SKShader Rückgabewert aus der statischen SKShader.CreateLinearGradient Methode zugewiesen. Die fünf Argumente sind wie folgt:

  • Der Anfangspunkt des Farbverlaufs, hier auf die obere linke Ecke des Rechtecks festgelegt
  • Der Endpunkt des Farbverlaufs, hier auf die untere rechte Ecke des Rechtecks festgelegt
  • Ein Array von zwei oder mehr Farben, die zum Farbverlauf beitragen
  • Ein Array von float Werten, das die relative Position der Farben innerhalb der Farbverlaufslinie angibt
  • Ein Element der SKShaderTileMode Enumeration, das angibt, wie sich der Farbverlauf über die Enden der Farbverlaufslinie hinaus verhält

Nachdem das Farbverlaufsobjekt erstellt wurde, zeichnet die DrawRect Methode das quadratische Rechteck mit 300 Pixeln mithilfe des SKPaint Objekts, das den Shader enthält. Hier wird es unter iOS, Android und dem Universelle Windows-Plattform (UWP) ausgeführt:

Farbverlauf von Ecken zu Ecke

Die Farbverlaufslinie wird durch die beiden ersten Argumente definiert. Beachten Sie, dass diese Punkte relativ zum Zeichenbereich und nicht zum grafischen Objekt sind, das mit dem Farbverlauf angezeigt wird. Entlang der Farbverlaufslinie wechselt die Farbe allmählich von Rot oben links zu Blau unten rechts. Jede Linie, die senkrecht zur Farbverlaufslinie steht, weist eine konstante Farbe auf.

Das Array von float Werten, die als viertes Argument angegeben werden, weist eine 1:1-Entsprechung mit dem Array von Farben auf. Die Werte geben die relative Position entlang der Farbverlaufslinie an, in der diese Farben auftreten. Hier bedeutet die 0, dass Red am Anfang der Farbverlaufslinie auftritt, und 1 bedeutet, dass Blue am Ende der Linie auftritt. Die Zahlen müssen aufsteigend sein und sich im Bereich von 0 bis 1 befinden. Wenn sie sich nicht in diesem Bereich befinden, werden sie an diesen Bereich angepasst.

Die beiden Werte im Array können auf einen anderen Wert als 0 und 1 festgelegt werden. Versuchen Sie Folgendes:

new float[] { 0.25f, 0.75f }

Nun ist das gesamte erste Quartal der Farbverlaufslinie rein rot, und das letzte Quartal ist rein blau. Die Mischung aus Rot und Blau ist auf die zentrale Hälfte der Farbverlauflinie beschränkt.

Im Allgemeinen sollten Sie diese Positionswerte gleichmäßig zwischen 0 und 1 platzieren. Wenn dies der Fall ist, können Sie einfach als viertes Argument angebennull.CreateLinearGradient

Obwohl dieser Farbverlauf zwischen zwei Ecken des quadratischen Rechtecks von 300 Pixeln definiert ist, ist er nicht auf das Ausfüllen dieses Rechtecks beschränkt. Die Seite "Eck-zu-Ecke-Farbverlauf " enthält zusätzlichen Code, der auf Tippen oder Mausklicks auf der Seite reagiert. Das drawBackground Feld wird zwischen true und false mit jedem Tippen umgeschaltet. Wenn der Wert lautet true, verwendet der PaintSurface Handler dasselbe SKPaint Objekt, um den gesamten Zeichenbereich auszufüllen, und zeichnet dann ein schwarzes Rechteck, das das kleinere Rechteck angibt:

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);
            }
        }
    }
}

Hier sehen Sie, was Nach dem Tippen auf den Bildschirm angezeigt wird:

Eck-zu-Ecke-Farbverlauf vollständig

Beachten Sie, dass sich der Farbverlauf in demselben Muster über die Punkte hinaus wiederholt, die die Farbverlaufslinie definieren. Diese Wiederholung tritt auf, da das letzte Argument CreateLinearGradient ist SKShaderTileMode.Repeat. (In Kürze werden die anderen Optionen angezeigt.)

Beachten Sie außerdem, dass die Punkte, mit denen Sie die Farbverlaufslinie angeben, nicht eindeutig sind. Linien, die senkrecht zur Farbverlaufslinie stehen, weisen dieselbe Farbe auf, sodass es eine unendliche Anzahl von Farbverlaufslinien gibt, die Sie für denselben Effekt angeben können. Wenn Sie z. B. ein Rechteck mit einem horizontalen Farbverlauf ausfüllen, können Sie die oberen linken und oberen rechten Ecken oder die unteren linken und unteren rechten Ecken oder zwei Punkte angeben, die sogar mit und parallel zu diesen Linien liegen.

Interaktives Experimentieren

Sie können interaktiv mit linearen Farbverläufen mit der Seite "Interaktiver linearer Farbverlauf " experimentieren. Diese Seite verwendet die InteractivePage klasse, die im Artikel Drei Methoden zum Zeichnen eines Bogens eingeführt wurde. InteractivePage Behandelt TouchEffect Ereignisse, um eine Sammlung von TouchPoint Objekten zu Standard, die Sie mit Den Fingern oder der Maus bewegen können.

Die XAML-Datei fügt das TouchEffect übergeordnete Element des Elements und SKCanvasView enthält außerdem eine Picker , mit der Sie eines der drei Elemente der SKShaderTileMode Enumeration auswählen können:

<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>

Der Konstruktor in der CodeBehind-Datei erstellt zwei TouchPoint Objekte für die Anfangs- und Endpunkte des linearen Farbverlaufs. Der PaintSurface Handler definiert ein Array von drei Farben (für einen Farbverlauf von Rot zu Grün zu Blau) und ruft den Aktuellen SKShaderTileMode aus dem Picker:

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);
        }
        ···
    }
}

Der PaintSurface Handler erstellt das SKShader Objekt aus allen diesen Informationen und verwendet es, um den gesamten Zeichenbereich zu färben. Das Array von float Werten wird auf null. Andernfalls legen Sie diesen Parameter auf ein Array mit den Werten 0, 0,5 und 1 fest.

Der Großteil des PaintSurface Handlers dient zum Anzeigen mehrerer Objekte: die Berührungspunkte als Gliederungskreise, die Farbverlaufslinie und die Linien senkrecht zu den Farbverlaufslinien an den Berührungspunkten:

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);
        }
    }
}

Die Farbverlaufslinie, die die beiden Berührungspunkte verbindet, ist leicht zu zeichnen, aber die senkrechten Linien erfordern etwas mehr Arbeit. Die Farbverlaufslinie wird in einen Vektor konvertiert, normalisiert, um eine Länge von einer Einheit zu haben, und dann um 90 Grad gedreht. Dieser Vektor erhält dann eine Länge von 200 Pixeln. Es wird verwendet, um vier Linien zu zeichnen, die sich von den Berührungspunkten erstrecken, um senkrecht zur Farbverlaufslinie zu sein.

Die senkrechten Linien stimmen mit dem Anfang und Ende des Farbverlaufs überein. Was über diese Zeilen hinaus geschieht, hängt von der Einstellung der SKShaderTileMode Enumeration ab:

Interaktiver linearer Farbverlauf

Die drei Screenshots zeigen die Ergebnisse der drei verschiedenen Werte von SKShaderTileMode. Der iOS-Screenshot zeigt SKShaderTileMode.Clamp, der nur die Farben auf dem Rahmen des Farbverlaufs erweitert. Die SKShaderTileMode.Repeat Option im Android-Screenshot zeigt, wie das Farbverlaufsmuster wiederholt wird. Die SKShaderTileMode.Mirror Option im UWP-Screenshot wiederholt auch das Muster, aber das Muster wird jedes Mal umgekehrt, was zu keinen Farbabbrüchen führt.

Farbverläufe auf Farbverläufen

Die SKShader Klasse definiert keine öffentlichen Eigenschaften oder Methoden mit Ausnahme von Dispose. Die SKShader Objekte, die mit ihren statischen Methoden erstellt wurden, sind daher unveränderlich. Selbst wenn Sie denselben Farbverlauf für zwei verschiedene Objekte verwenden, ist es wahrscheinlich, dass Sie den Farbverlauf geringfügig variieren möchten. Dazu müssen Sie ein neues SKShader Objekt erstellen.

Auf der Seite "Farbverlaufstext " werden Text und ein Geschweift angezeigt, der beide mit ähnlichen Farbverläufen gefärbt sind:

Farbverlaufstext

Die einzigen Unterschiede bei den Farbverläufen sind die Anfangs- und Endpunkte. Der zum Anzeigen von Text verwendete Farbverlauf basiert auf zwei Punkten auf den Ecken des umgebenden Rechtecks für den Text. Für den Hintergrund basieren die beiden Punkte auf dem gesamten Zeichenbereich. Hier ist der -Code:

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);
        }
    }
}

Die Shader Eigenschaft des SKPaint Objekts wird zuerst so festgelegt, dass ein Farbverlauf angezeigt wird, um den Hintergrund abzudecken. Die Farbverlaufspunkte werden auf die oberen linken und unteren rechten Ecken des Zeichenbereichs festgelegt.

Der Code legt die TextSize Eigenschaft des SKPaint Objekts so fest, dass der Text bei 90 % der Breite des Zeichenbereichs angezeigt wird. Die Textgrenzen werden zum Berechnen und Berechnen xText von yText Werten verwendet, die an die DrawText Methode übergeben werden, um den Text zu zentrieren.

Die Farbverlaufspunkte für den zweiten CreateLinearGradient Aufruf müssen jedoch auf die obere linke und untere rechte Ecke des Texts relativ zum Zeichenbereich verweisen, wenn er angezeigt wird. Dies wird erreicht, indem das textBounds Rechteck um die gleichen xText Werte yText verschoben wird:

textBounds.Offset(xText, yText);

Jetzt können die oberen linken und unteren rechten Ecken des Rechtecks verwendet werden, um die Anfangs- und Endpunkte des Farbverlaufs festzulegen.

Animieren eines Farbverlaufs

Es gibt mehrere Möglichkeiten zum Animieren eines Farbverlaufs. Ein Ansatz besteht darin, die Anfangs- und Endpunkte zu animieren. Die Seite "Farbverlaufsanimation " verschiebt die beiden Punkte in einem Kreis, der auf dem Zeichenbereich zentriert ist. Der Radius dieses Kreises ist die Hälfte der Breite oder Höhe des Zeichenbereichs, je nachdem, welcher Wert kleiner ist. Die Anfangs- und Endpunkte stehen sich auf diesem Kreis gegenüber, und der Farbverlauf wechselt von Weiß zu Schwarz mit einem Mirror Kachelmodus:

Farbverlaufsanimation

Der Konstruktor erstellt die SKCanvasView. Die OnAppearing Methoden behandeln OnDisappearing die Animationslogik:

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;
    }
    ···
}

Die OnTimerTick Methode berechnet einen angle Wert, der von 0 bis 2π alle 3 Sekunden animiert wird.

Hier ist eine Möglichkeit, die beiden Farbverlaufspunkte zu berechnen. Ein benannter SKPointvector Wert wird berechnet, um von der Mitte des Zeichenbereichs auf einen Punkt auf den Radius des Kreises zu erweitern. Die Richtung dieses Vektors basiert auf den Sinus- und Kosinuswerten des Winkels. Die beiden gegenüberliegenden Farbverlaufspunkte werden dann berechnet: Ein Punkt wird berechnet, indem dieser Vektor vom Mittelpunkt subtrahiert wird, und ein anderer Punkt wird berechnet, indem der Vektor zum Mittelpunkt hinzugefügt wird:

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);
        }
    }
}

Ein etwas anderer Ansatz erfordert weniger Code. Bei diesem Ansatz wird die SKShader.CreateLinearGradient Überladungsmethode mit einer Matrixtransformation als letztes Argument verwendet. Dieser Ansatz ist die Version im Beispiel:

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);
        }
    }
}

Wenn die Breite des Zeichenbereichs kleiner als die Höhe ist, werden die beiden Farbverlaufspunkte auf (0, 0) und (info.Width0) festgelegt. Die Drehungstransformation, die als letztes Argument übergeben wurde, um CreateLinearGradient diese beiden Punkte effektiv um die Mitte des Bildschirms zu drehen.

Wenn der Winkel 0 ist, gibt es keine Drehung, und die beiden Farbverlaufspunkte sind die oberen linken und oberen rechten Ecken des Zeichenbereichs. Diese Punkte sind nicht die gleichen Farbverlaufspunkte, die wie im vorherigen CreateLinearGradient Aufruf dargestellt werden. Diese Punkte sind jedoch parallel zur horizontalen Farbverlaufslinie, die die Mitte des Zeichenbereichs abschneidet und zu einem identischen Farbverlauf führt.

Regenbogenverlauf

Das Zeichenblatt "Regenbogenverlauf " zeichnet einen Regenbogen aus der oberen linken Ecke des Zeichenbereichs in die untere rechte Ecke. Aber dieser Regenbogenverlauf ist nicht wie ein echter Regenbogen. Es ist gerade und nicht gekrümmt, sondern basiert auf acht HSL-Farben (Farbtonsättigung-Leuchtdichte), die durch das Durchlaufen von Farbtonwerten von 0 bis 360 bestimmt werden:

SKColor[] colors = new SKColor[8];

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

Dieser Code ist Teil des PaintSurface unten gezeigten Handlers. Der Handler beginnt mit dem Erstellen eines Pfads, der ein sechsseitiges Polygon definiert, das sich von der oberen linken Ecke des Zeichenbereichs bis zur unteren rechten Ecke erstreckt:

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);
            }
        }
    }
}

Die beiden Farbverlaufspunkte in der CreateLinearGradient Methode basieren auf zwei der Punkte, die diesen Pfad definieren: Beide Punkte befinden sich in der Nähe der oberen linken Ecke. Der erste befindet sich am oberen Rand des Zeichenbereichs und der zweite befindet sich am linken Rand des Zeichenbereichs. Das Ergebnis lautet wie folgt:

Regenbogenverlauf fehlerhaft

Dies ist ein interessantes Bild, aber es ist nicht ganz die Absicht. Das Problem besteht darin, dass beim Erstellen eines linearen Farbverlaufs die Linien der Konstantenfarbe senkrecht zur Farbverlaufslinie stehen. Die Farbverlaufslinie basiert auf den Punkten, an denen die Figur die obere und linke Seite berührt, und diese Linie ist in der Regel nicht senkrecht zu den Rändern der Abbildung, die sich auf die untere rechte Ecke erstrecken. Dieser Ansatz würde nur funktionieren, wenn die Canvas quadratisch wäre.

Um einen richtigen Regenbogenverlauf zu erstellen, muss die Farbverlaufslinie senkrecht zum Rand des Regenbogens sein. Das ist eine komplexere Berechnung. Ein Vektor muss definiert werden, der parallel zur langen Seite der Abbildung ist. Der Vektor wird um 90 Grad gedreht, sodass er senkrecht zu dieser Seite ist. Es wird dann verlängert, um die Breite der Zahl durch Multiplizieren mit rainbowWidth. Die beiden Farbverlaufspunkte werden basierend auf einem Punkt auf der Seite der Abbildung und diesem Punkt plus dem Vektor berechnet. Im Folgenden sehen Sie den Code, der auf der Seite "Regenbogenverlauf " im Beispiel angezeigt wird:

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);
            }
        }
    }
}

Jetzt werden die Regenbogenfarben an der Figur ausgerichtet:

Regenbogenverlauf

Unendliche Farben

Ein Regenbogenverlauf wird auch auf der Infinity Colors-Seite verwendet. Diese Seite zeichnet ein Unendlichkeitszeichen mit einem Pfadobjekt, das im Artikel Drei Arten von Bézierkurven beschrieben wird. Das Bild wird dann mit einem animierten Regenbogenfarbverlauf gefärbt, der sich kontinuierlich über das Bild erstreckt.

Der Konstruktor erstellt das Objekt, das SKPath das Unendlichkeitszeichen beschreibt. Nachdem der Pfad erstellt wurde, kann der Konstruktor auch die rechteckigen Grenzen des Pfads abrufen. Anschließend wird ein Wert berechnet, der aufgerufen wird gradientCycleLength. Wenn ein Farbverlauf auf den oberen linken und unteren rechten Ecken des pathBounds Rechtecks basiert, ist dieser gradientCycleLength Wert die gesamt horizontale Breite des Farbverlaufsmusters:

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;
    }
    ···
}

Der Konstruktor erstellt außerdem das colors Array für den Regenbogen und das SKCanvasView Objekt.

Außerkraftsetzungen der OnAppearing Und OnDisappearing Methoden führen den Aufwand für die Animation aus. Die OnTimerTick Methode animiert das offset Feld von 0 bis gradientCycleLength alle zwei Sekunden:

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;
    }
    ···
}

Schließlich rendert der PaintSurface Handler das Unendlichkeitszeichen. Da der Pfad negative und positive Koordinaten enthält, die einen Mittelpunkt (0, 0) umgeben, wird eine Translate Transformation auf der Canvas verwendet, um sie in die Mitte zu verschieben. Auf die Übersetzungstransformation folgt eine Scale Transformation, die einen Skalierungsfaktor anwendet, der das Unendlichkeitszeichen so groß wie möglich macht, während sie immer noch innerhalb von 95 % der Breite und Höhe des Zeichenbereichs bleiben.

Beachten Sie, dass die STROKE_WIDTH Konstante der Breite und Höhe des umgebenden Pfadrechtecks hinzugefügt wird. Der Pfad wird mit einer Linie dieser Breite strichen, sodass die Größe der gerenderten Unendlichkeitsgröße um die Hälfte dieser Breite auf allen vier Seiten erhöht wird:

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);
        }
    }
}

Sehen Sie sich die punkte an, die als die ersten beiden Argumente von SKShader.CreateLinearGradient. Diese Punkte basieren auf dem ursprünglichen pfadgebundenen Rechteck. Der erste Punkt ist (–250, –100) und der zweite ist (250, 100). Intern in SkiaSharp werden diese Punkte der aktuellen Canvastransformation unterzogen, sodass sie korrekt an dem angezeigten Unendlichkeitszeichen ausgerichtet sind.

Ohne das letzte Argument zu CreateLinearGradientsehen, sehen Sie einen Regenbogenverlauf, der sich von der oberen linken Seite des Unendlichkeitszeichens bis zur unteren rechten Seite erstreckt. (Tatsächlich erstreckt sich der Farbverlauf von der oberen linken Ecke zur unteren rechten Ecke des umgebenden Rechtecks. Das gerenderte Unendlichkeitszeichen ist größer als das umgebende Rechteck um die Hälfte des STROKE_WIDTH Werts auf allen Seiten. Da der Farbverlauf sowohl am Anfang als auch am Ende rot ist und der Farbverlauf erstellt SKShaderTileMode.Repeatwird, ist der Unterschied nicht erkennbar.)

Mit diesem letzten Argument wird CreateLinearGradientdas Farbverlaufsmuster kontinuierlich über das Bild verteilt:

Unendliche Farben

Transparenz und Farbverläufe

Die Farben, die zu einem Farbverlauf beitragen, können Transparenz enthalten. Anstelle eines Farbverlaufs, der von einer Farbe zu einer anderen ausgeblendet wird, kann der Farbverlauf von einer Farbe zu transparent verblassen.

Sie können diese Technik für einige interessante Effekte verwenden. Eines der klassischen Beispiele zeigt ein grafisches Objekt mit seiner Reflexion:

Spiegelungsverlauf

Der Text, der auf dem Kopf steht, wird mit einem Farbverlauf gefärbt, der oben 50 % transparent ist, um am unteren Rand vollständig transparent zu sein. Diese Transparenzstufen sind Alphawerte von 0x80 und 0 zugeordnet.

Der PaintSurface Handler auf der Seite "Spiegelungsverlauf " skaliert die Größe des Texts auf 90 % der Breite des Zeichenbereichs. Anschließend werden berechnet und Werte berechnet xText , yText um den Text horizontal zentriert zu positionieren, wobei er jedoch auf einem Basisplan sitzt, der der vertikalen Mitte der Seite entspricht:

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);
        }
    }
}

Diese xText und yText Werte sind die gleichen Werte, die zum Anzeigen des reflektierten Texts im DrawText Aufruf am unteren Rand des PaintSurface Handlers verwendet werden. Kurz vor diesem Code wird jedoch ein Aufruf der Scale Methode angezeigt SKCanvas. Diese Scale Methode skaliert horizontal um 1 (was nichts tut), aber vertikal nach –1, wodurch alles auf den Kopf gedreht wird. Die Drehmitte wird auf den Punkt (0) festgelegt, yTextwobei yText es sich um die vertikale Mitte des Zeichenbereichs handelt, die ursprünglich als info.Height dividiert durch 2 berechnet wurde.

Beachten Sie, dass Skia den Farbverlauf verwendet, um grafische Objekte vor den Canvastransformationen zu farbieren. Nachdem der unreflektierte Text gezeichnet wurde, wird das textBounds Rechteck verschoben, sodass es dem angezeigten Text entspricht:

textBounds.Offset(xText, yText);

Der CreateLinearGradient Aufruf definiert einen Farbverlauf vom oberen Rand des Rechtecks zum unteren Rand. Der Farbverlauf ist von einem vollständig transparenten Blau (paint.Color.WithAlpha(0)) zu einem 50% transparenten Blau (paint.Color.WithAlpha(0x80)). Die Canvastransformation kippt den Text nach unten, sodass das transparente Blau von 50 % am Basisplan beginnt und am oberen Rand des Texts transparent wird.