Condividi tramite


Sfumatura lineare SkiaSharp

La SKPaint classe definisce una Color proprietà utilizzata per tracciare linee o aree di riempimento con un colore a tinta unita. In alternativa, è possibile tracciare linee o riempire aree con sfumature, che sono graduali sfumature di colori:

Esempio di sfumatura lineare

Il tipo più semplice di sfumatura è una sfumatura lineare . La miscela di colori si verifica su una linea (denominata linea sfumata) da un punto a un altro. Le linee perpendicolare alla linea sfumata hanno lo stesso colore. Si crea una sfumatura lineare usando uno dei due metodi statici SKShader.CreateLinearGradient . La differenza tra i due overload è che uno include una trasformazione matrice e l'altra non.

Questi metodi restituiscono un oggetto di tipo SKShader impostato sulla Shader proprietà di SKPaint. Se la Shader proprietà è diversa da Null, esegue l'override della Color proprietà . Qualsiasi linea tracciata o qualsiasi area riempita utilizzando questo SKPaint oggetto si basa sulla sfumatura anziché sul colore a tinta unita.

Nota

La Shader proprietà viene ignorata quando si include un SKPaint oggetto in una DrawBitmap chiamata. È possibile usare la Color proprietà di per impostare un livello di SKPaint trasparenza per la visualizzazione di una bitmap (come descritto nell'articolo Visualizzazione di bitmap SkiaSharp), ma non è possibile usare la Shader proprietà per visualizzare una bitmap con una trasparenza sfumata. Altre tecniche sono disponibili per la visualizzazione di bitmap con trasparenze sfumature: queste sono descritte negli articoli Sfumature circolari SkiaSharp e Modalità di composizione e fusione SkiaSharp.

Sfumature da angolo a angolo

Spesso una sfumatura lineare si estende da un angolo di un rettangolo a un altro. Se il punto iniziale è l'angolo superiore sinistro del rettangolo, la sfumatura può estendersi:

  • verticalmente all'angolo inferiore sinistro
  • orizzontalmente all'angolo superiore destro
  • diagonalmente all'angolo inferiore destro

La sfumatura lineare diagonale viene illustrata nella prima pagina della sezione SkiaSharp Shader e Altri effetti dell'esempio. La pagina Sfumatura da angolo a angolo crea un oggetto SKCanvasView nel relativo costruttore. Il PaintSurface gestore crea un SKPaint oggetto in un'istruzione using e quindi definisce un rettangolo quadrato di 300 pixel centrato nell'area di disegno:

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

Alla Shader proprietà di SKPaint viene assegnato il SKShader valore restituito dal metodo statico SKShader.CreateLinearGradient . I cinque argomenti sono i seguenti:

  • Punto iniziale della sfumatura, impostato qui sull'angolo superiore sinistro del rettangolo
  • Punto finale della sfumatura, impostato qui sull'angolo inferiore destro del rettangolo
  • Matrice di due o più colori che contribuiscono alla sfumatura
  • Matrice di float valori che indica la posizione relativa dei colori all'interno della linea sfumata
  • Membro dell'enumerazione SKShaderTileMode che indica il comportamento della sfumatura oltre le estremità della linea sfumata

Dopo aver creato l'oggetto sfumatura, il DrawRect metodo disegna il rettangolo quadrato di 300 pixel usando l'oggetto SKPaint che include lo shader. Qui è in esecuzione in iOS, Android e la piattaforma UWP (Universal Windows Platform) (UWP):

Sfumatura da angolo a angolo

La linea sfumata è definita dai due punti specificati come primi due argomenti. Si noti che questi punti sono relativi all'area di disegno e non all'oggetto grafico visualizzato con la sfumatura. Lungo la linea sfumata, il colore passa gradualmente dal rosso in alto a sinistra al blu in basso a destra. Qualsiasi linea perpendicolare alla linea sfumata ha un colore costante.

La matrice di float valori specificata come quarto argomento ha una corrispondenza uno-a-uno con la matrice di colori. I valori indicano la posizione relativa lungo la linea sfumata in cui si verificano tali colori. In questo caso, il valore 0 indica che Red si verifica all'inizio della linea sfumata e 1 indica che Blue si verifica alla fine della linea. I numeri devono essere crescente e devono essere compresi nell'intervallo compreso tra 0 e 1. Se non si trovano in tale intervallo, verranno modificati in base a tale intervallo.

I due valori nella matrice possono essere impostati su un valore diverso da 0 e 1. Provare a eseguire quanto segue:

new float[] { 0.25f, 0.75f }

Ora l'intero primo trimestre della linea sfumatura è rosso puro e l'ultimo trimestre è blu puro. Il mix di rosso e blu è limitato alla metà centrale della linea sfumata.

In genere, è consigliabile spaziare questi valori di posizione ugualmente da 0 a 1. In questo caso, è sufficiente specificare null come quarto argomento a CreateLinearGradient.

Anche se questa sfumatura è definita tra due angoli del rettangolo quadrato di 300 pixel, non è limitata a riempire tale rettangolo. La pagina Sfumatura da angolo a angolo include codice aggiuntivo che risponde ai tocco o ai clic del mouse nella pagina. Il drawBackground campo viene alternato true tra e false con ogni tocco. Se il valore è true, il PaintSurface gestore usa lo stesso SKPaint oggetto per riempire l'intera area di disegno e quindi disegna un rettangolo nero che indica il rettangolo più piccolo:

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

Ecco cosa vedrai dopo aver toccato la schermata:

Sfumatura da angolo a angolo piena

Si noti che la sfumatura si ripete nello stesso modello oltre i punti che definiscono la linea sfumata. Questa ripetizione si verifica perché l'ultimo argomento di CreateLinearGradient è SKShaderTileMode.Repeat. Verranno visualizzate le altre opzioni a breve.

Si noti anche che i punti usati per specificare la linea sfumata non sono univoci. Le linee perpendicolare alla linea sfumato hanno lo stesso colore, quindi esiste un numero infinito di linee sfumature che è possibile specificare per lo stesso effetto. Ad esempio, quando si riempie un rettangolo con una sfumatura orizzontale, è possibile specificare gli angoli superiore sinistro e superiore destro oppure gli angoli inferiore sinistro e inferiore destro o qualsiasi due punti con e paralleli a tali linee.

Esperimento interattivo

È possibile sperimentare in modo interattivo le sfumature lineari con la pagina Sfumatura lineare interattiva. Questa pagina usa la InteractivePage classe introdotta nell'articolo Tre modi per disegnare un arco. InteractivePage Gestisce TouchEffect gli eventi per mantenere una raccolta di TouchPoint oggetti che è possibile spostare con le dita o con il mouse.

Il file XAML allega a TouchEffect un elemento padre di SKCanvasView e include anche un oggetto Picker che consente di selezionare uno dei tre membri dell'enumerazione SKShaderTileMode :

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

Il costruttore nel file code-behind crea due TouchPoint oggetti per i punti iniziale e finale della sfumatura lineare. Il PaintSurface gestore definisce una matrice di tre colori (per una sfumatura dal rosso al verde al blu) e ottiene l'oggetto Pickercorrente SKShaderTileMode da :

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

Il PaintSurface gestore crea l'oggetto SKShader da tutte le informazioni e lo usa per colorare l'intera area di disegno. La matrice di float valori è impostata su null. In caso contrario, per spaziare in modo uniforme tre colori, è necessario impostare tale parametro su una matrice con i valori 0, 0,5 e 1.

La maggior parte del PaintSurface gestore è dedicata alla visualizzazione di diversi oggetti: i punti di tocco come cerchi di contorno, la linea sfumata e le linee perpendicolare alle linee sfumature nei punti di tocco:

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

La linea sfumata che collega i due punti di tocco è facile da disegnare, ma le linee perpendicolare richiedono un po ' di lavoro. La linea sfumata viene convertita in un vettore, normalizzata in modo da avere una lunghezza di un'unità e quindi ruotata di 90 gradi. A tale vettore viene quindi assegnata una lunghezza di 200 pixel. Viene usato per disegnare quattro linee che si estendono dai punti di tocco perpendicolare alla linea della sfumatura.

Le linee perpendicolare coincidono con l'inizio e la fine della sfumatura. Ciò che accade oltre tali righe dipende dall'impostazione dell'enumerazione SKShaderTileMode :

Sfumatura lineare interattiva

I tre screenshot mostrano i risultati dei tre diversi valori di SKShaderTileMode. Lo screenshot di iOS mostra SKShaderTileMode.Clamp, che estende solo i colori sul bordo della sfumatura. L'opzione SKShaderTileMode.Repeat nello screenshot di Android mostra come viene ripetuto il modello di sfumatura. L'opzione SKShaderTileMode.Mirror nello screenshot UWP ripete anche il modello, ma il motivo viene invertito ogni volta, senza interruzioni di colore.

Sfumature sulle sfumature

La SKShader classe non definisce proprietà o metodi pubblici, ad eccezione di Dispose. Gli SKShader oggetti creati dai relativi metodi statici sono pertanto non modificabili. Anche se si usa la stessa sfumatura per due oggetti diversi, è probabile che si voglia variare leggermente la sfumatura. A tale scopo, è necessario creare un nuovo SKShader oggetto.

La pagina Testo sfumato visualizza testo e un primo piano che sono entrambi colorati con sfumature simili:

Testo sfumato

Le uniche differenze nelle sfumature sono i punti iniziale e finale. La sfumatura utilizzata per la visualizzazione del testo si basa su due punti sugli angoli del rettangolo di delimitazione per il testo. Per lo sfondo, i due punti si basano sull'intera area di disegno. Ecco il codice:

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

La Shader proprietà dell'oggetto SKPaint viene impostata prima per visualizzare una sfumatura per coprire lo sfondo. I punti sfumato vengono impostati negli angoli superiore sinistro e inferiore destro dell'area di disegno.

Il codice imposta la TextSize proprietà dell'oggetto SKPaint in modo che il testo venga visualizzato al 90% della larghezza dell'area di disegno. I limiti di testo vengono usati per calcolare xText e yText i valori da passare al DrawText metodo per centrare il testo.

Tuttavia, i punti sfumatura per la seconda CreateLinearGradient chiamata devono fare riferimento all'angolo superiore sinistro e inferiore destro del testo rispetto all'area di disegno quando viene visualizzato. A tale scopo, spostare il textBounds rettangolo in base agli stessi xText valori e yText :

textBounds.Offset(xText, yText);

È ora possibile usare gli angoli superiore sinistro e inferiore destro del rettangolo per impostare i punti iniziale e finale della sfumatura.

Animazione di una sfumatura

Esistono diversi modi per animare una sfumatura. Un approccio consiste nell'animare i punti iniziale e finale. La pagina Animazione sfumatura sposta i due punti intorno in un cerchio centrato sull'area di disegno. Il raggio di questo cerchio è metà della larghezza o dell'altezza dell'area di disegno, a seconda di quale sia minore. I punti iniziale e finale si trovano di fronte a questo cerchio e la sfumatura passa da bianco a nero con una Mirror modalità riquadro:

Animazione sfumatura

Il costruttore crea l'oggetto SKCanvasView. I OnAppearing metodi e OnDisappearing gestiscono la logica di animazione:

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

Il OnTimerTick metodo calcola un angle valore animato da 0 a 2π ogni 3 secondi.

Ecco un modo per calcolare i due punti sfumatura. Viene calcolato un SKPoint valore denominato vector per estendersi dal centro dell'area di disegno a un punto sul raggio del cerchio. La direzione di questo vettore si basa sui valori del seno e del coseno dell'angolo. I due punti sfumatura opposti vengono quindi calcolati: un punto viene calcolato sottraendo tale vettore dal punto centrale e un altro punto viene calcolato aggiungendo il vettore al punto centrale:

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

Un approccio leggermente diverso richiede meno codice. Questo approccio usa il SKShader.CreateLinearGradient metodo di overload con una trasformazione matrice come ultimo argomento. Questo approccio è la versione nell'esempio:

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

Se la larghezza dell'area di disegno è minore dell'altezza, i due punti sfumato vengono impostati su (0, 0) e (info.Width, 0). La trasformazione di rotazione passata come ultimo argomento per ruotare CreateLinearGradient efficacemente questi due punti intorno al centro dello schermo.

Si noti che se l'angolo è 0, non esiste alcuna rotazione e i due punti sfumato sono gli angoli superiore sinistro e superiore destro dell'area di disegno. Questi punti non sono gli stessi punti sfumatura calcolati come illustrato nella chiamata precedente CreateLinearGradient . Tuttavia, questi punti sono paralleli alla linea sfumata orizzontale che bisseca il centro dell'area di disegno e generano una sfumatura identica.

Sfumatura arcobaleno

La pagina Rainbow Gradient disegna un arcobaleno dall'angolo superiore sinistro dell'area di disegno all'angolo inferiore destro. Ma questa sfumatura arcobaleno non è come un vero arcobaleno. È dritto piuttosto che curvo, ma si basa su otto colori HSL (tonalità-saturazione-luminosità) determinati dal ciclo attraverso valori di tonalità compresi tra 0 e 360:

SKColor[] colors = new SKColor[8];

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

Questo codice fa parte del PaintSurface gestore illustrato di seguito. Il gestore inizia creando un percorso che definisce un poligono a sei lati che si estende dall'angolo superiore sinistro dell'area di disegno all'angolo inferiore destro:

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

I due punti sfumato nel CreateLinearGradient metodo si basano su due dei punti che definiscono questo percorso: entrambi i punti sono vicini all'angolo superiore sinistro. Il primo si trova sul bordo superiore dell'area di disegno e il secondo si trova sul bordo sinistro dell'area di disegno. Il risultato è il seguente:

Sfumatura arcobaleno difettosa

Questa è un'immagine interessante, ma non è proprio la finalità. Il problema è che quando si crea una sfumatura lineare, le linee di colore costante sono perpendicolare alla linea sfumata. La linea sfumata si basa sui punti in cui la figura tocca i lati superiore e sinistro e tale linea in genere non è perpendicolare ai bordi della figura che si estendono all'angolo inferiore destro. Questo approccio funziona solo se l'area di disegno fosse quadrata.

Per creare una sfumatura arcobaleno corretta, la linea sfumata deve essere perpendicolare al bordo dell'arcobaleno. Questo è un calcolo più coinvolto. È necessario definire un vettore parallelo al lato lungo della figura. Il vettore viene ruotato di 90 gradi in modo che sia perpendicolare a quel lato. Viene quindi allungato per essere la larghezza della figura moltiplicando per rainbowWidth. I due punti sfumato vengono calcolati in base a un punto sul lato della figura e a quel punto più il vettore. Di seguito è riportato il codice visualizzato nella pagina Sfumatura arcobaleno nell'esempio:

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

Ora i colori dell'arcobaleno sono allineati alla figura:

Sfumatura arcobaleno

Colori infinito

Nella pagina Colori infinito viene usata anche una sfumatura arcobaleno. Questa pagina disegna un segno infinito usando un oggetto path descritto nell'articolo Tre tipi di curve di Bézier. L'immagine viene quindi colorata con una sfumatura arcobaleno animata che spazza continuamente l'immagine.

Il costruttore crea l'oggetto SKPath che descrive il segno infinito. Dopo aver creato il percorso, il costruttore può anche ottenere i limiti rettangolari del percorso. Calcola quindi un valore denominato gradientCycleLength. Se una sfumatura si basa sugli angoli superiore sinistro e inferiore destro del pathBounds rettangolo, questo gradientCycleLength valore corrisponde alla larghezza orizzontale totale del modello di sfumatura:

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

Il costruttore crea anche la colors matrice per l'arcobaleno e l'oggetto SKCanvasView .

Gli override dei OnAppearing metodi e OnDisappearing e eseguono l'overhead per l'animazione. Il OnTimerTick metodo anima il offset campo da 0 a gradientCycleLength due secondi:

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

Infine, il gestore esegue il PaintSurface rendering del segno infinito. Poiché il percorso contiene coordinate negative e positive che circondano un punto centrale di (0, 0), viene usata una Translate trasformazione nell'area di disegno per spostarla al centro. La trasformazione di traslazione è seguita da una Scale trasformazione che applica un fattore di ridimensionamento che rende il segno infinito il più grande possibile pur rimanendo entro il 95% della larghezza e dell'altezza dell'area di disegno.

Si noti che la STROKE_WIDTH costante viene aggiunta alla larghezza e all'altezza del rettangolo di delimitazione del percorso. Il percorso verrà tracciato con una linea di questa larghezza, quindi le dimensioni della dimensione dell'infinito sottoposto a rendering vengono aumentate di metà della larghezza su tutti e quattro i lati:

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

Esaminare i punti passati come primi due argomenti di SKShader.CreateLinearGradient. Questi punti si basano sul rettangolo di delimitazione del percorso originale. Il primo punto è (–250, –100) e il secondo è (250, 100). Interno a SkiaSharp, questi punti sono soggetti alla trasformazione dell'area di disegno corrente in modo che siano allineati correttamente con il segno infinito visualizzato.

Senza l'ultimo argomento di CreateLinearGradient, si noterà una sfumatura arcobaleno che si estende dall'alto a sinistra del segno infinito verso il basso a destra. In realtà, la sfumatura si estende dall'angolo superiore sinistro all'angolo inferiore destro del rettangolo di delimitazione. Il segno infinito sottoposto a rendering è maggiore del rettangolo di delimitazione per metà del STROKE_WIDTH valore su tutti i lati. Poiché la sfumatura è rossa sia all'inizio che alla fine e la sfumatura viene creata con SKShaderTileMode.Repeat, la differenza non è evidente.

Con l'ultimo argomento su CreateLinearGradient, il modello di sfumatura esegue continuamente lo sweep nell'immagine:

Colori infinito

Trasparenza e sfumature

I colori che contribuiscono a una sfumatura possono incorporare la trasparenza. Invece di una sfumatura che si dissolve da un colore a un altro, la sfumatura può dissolversi da un colore a trasparente.

È possibile usare questa tecnica per alcuni effetti interessanti. Uno degli esempi classici mostra un oggetto grafico con la sua reflection:

Sfumatura reflection

Il testo capovolto è colorato con una sfumatura che è trasparente al 50% in alto per essere completamente trasparente nella parte inferiore. Questi livelli di trasparenza sono associati ai valori alfa di 0x80 e 0.

Il PaintSurface gestore nella pagina Sfumatura reflection ridimensiona le dimensioni del testo al 90% della larghezza dell'area di disegno. Calcola quindi xText i valori e yText per posizionare il testo al centro orizzontalmente, ma seduto su una linea di base corrispondente al centro verticale della pagina:

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

Tali xText valori e yText sono gli stessi valori usati per visualizzare il testo riflesso nella DrawText chiamata nella parte inferiore del PaintSurface gestore. Subito prima di tale codice, tuttavia, verrà visualizzata una chiamata al Scale metodo di SKCanvas. Questo Scale metodo viene ridimensionato orizzontalmente di 1 (che non fa nulla), ma verticalmente di -1, che in effetti capovolge tutto. Il centro di rotazione viene impostato sul punto (0, yText), dove yText è il centro verticale dell'area di disegno, originariamente calcolato come info.Height diviso per 2.

Tenere presente che Skia usa la sfumatura per colorare gli oggetti grafici prima delle trasformazioni dell'area di disegno. Dopo aver disegnato il testo non riflesso, il textBounds rettangolo viene spostato in modo che corrisponda al testo visualizzato:

textBounds.Offset(xText, yText);

La CreateLinearGradient chiamata definisce una sfumatura dalla parte superiore del rettangolo verso il basso. La sfumatura è da un blu completamente trasparente (paint.Color.WithAlpha(0)) a un blu trasparente al 50% (paint.Color.WithAlpha(0x80)). La trasformazione canvas capovolge il testo capovolto, quindi il 50% di blu trasparente inizia alla linea di base e diventa trasparente all'inizio del testo.