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:
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):
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:
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 Picker
corrente 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
:
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:
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:
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:
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:
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:
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:
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.