Informazioni sui tracciati ed enumerazione
Ottenere informazioni sui percorsi ed enumerare il contenuto
La SKPath
classe definisce diverse proprietà e metodi che consentono di ottenere informazioni sul percorso. Le Bounds
proprietà e TightBounds
(e i metodi correlati) ottengono le dimensioni metricali di un percorso. Il Contains
metodo consente di determinare se un punto specifico si trova all'interno di un percorso.
A volte è utile determinare la lunghezza totale di tutte le linee e le curve che costituiscono un tracciato. Il calcolo di questa lunghezza non è un'attività semplice in modo algoritmico, quindi un'intera classe denominata PathMeasure
è dedicata.
A volte è utile anche ottenere tutte le operazioni e i punti di disegno che costituiscono un percorso. In un primo momento, questa funzionalità potrebbe sembrare non necessaria: se il programma ha creato il percorso, il programma conosce già il contenuto. Tuttavia, si è visto che i percorsi possono essere creati anche dagli effetti del percorso e convertendo le stringhe di testo in percorsi. È anche possibile ottenere tutte le operazioni e i punti di disegno che costituiscono questi percorsi. Una possibilità consiste nell'applicare una trasformazione algoritmica a tutti i punti, ad esempio per eseguire il wrapping del testo intorno a un emisfero:
Recupero della lunghezza del percorso
Nell'articolo Percorsi e testo è stato illustrato come usare il DrawTextOnPath
metodo per disegnare una stringa di testo la cui linea di base segue il corso di un percorso. Ma cosa succede se si vuole ridimensionare il testo in modo che si adatti esattamente al percorso? Il disegno di testo intorno a un cerchio è facile perché la circonferenza di un cerchio è semplice da calcolare. Ma la circonferenza di un'ellisse o la lunghezza di una curva di Bézier non è così semplice.
La SKPathMeasure
classe può essere utile. Il costruttore accetta un SKPath
argomento e la Length
proprietà ne rivela la lunghezza.
Questa classe è illustrata nell'esempio Path Length , basato sulla pagina Curva di Bézier. Il file PathLengthPage.xaml deriva da InteractivePage
e include un'interfaccia touch:
<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.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
Title="Path Length">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</local:InteractivePage>
Il file code-behind PathLengthPage.xaml.cs consente di spostare quattro punti di tocco per definire i punti finali e i punti di controllo di una curva cubica di Bézier. Tre campi definiscono una stringa di testo, un SKPaint
oggetto e una larghezza calcolata del testo:
public partial class PathLengthPage : InteractivePage
{
const string text = "Compute length of path";
static SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Black,
TextSize = 10,
};
static readonly float baseTextWidth = textPaint.MeasureText(text);
...
}
Il baseTextWidth
campo è la larghezza del testo in base a un'impostazione TextSize
pari a 10.
Il PaintSurface
gestore disegna la curva di Bézier e quindi ridimensiona il testo per adattarlo alla lunghezza intera:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw path with cubic Bezier curve
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.CubicTo(touchPoints[1].Center,
touchPoints[2].Center,
touchPoints[3].Center);
canvas.DrawPath(path, strokePaint);
// Get path length
SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
// Find new text size
textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
// Draw text on path
canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
}
...
}
La Length
proprietà dell'oggetto appena creato SKPathMeasure
ottiene la lunghezza del percorso. La lunghezza del percorso è divisa per il baseTextWidth
valore (ovvero la larghezza del testo in base a una dimensione del testo pari a 10) e quindi moltiplicata per la dimensione del testo di base pari a 10. Il risultato è una nuova dimensione del testo per la visualizzazione del testo lungo il percorso:
Man mano che la curva di Bézier diventa più lunga o più breve, è possibile visualizzare la modifica delle dimensioni del testo.
Attraversamento del percorso
SKPathMeasure
può fare più di misurare solo la lunghezza del percorso. Per qualsiasi valore compreso tra zero e la lunghezza del percorso, un SKPathMeasure
oggetto può ottenere la posizione sul percorso e la tangente alla curva di percorso in quel punto. La tangente è disponibile come vettore sotto forma di SKPoint
oggetto o come rotazione incapsulata in un SKMatrix
oggetto . Ecco i metodi di SKPathMeasure
che ottengono queste informazioni in modi diversi e flessibili:
Boolean GetPosition (Single distance, out SKPoint position)
Boolean GetTangent (Single distance, out SKPoint tangent)
Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)
Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)
I membri dell'enumerazione SKPathMeasureMatrixFlags
sono:
GetPosition
GetTangent
GetPositionAndTangent
La pagina Unicycle Half-Pipe anima una figura di bastone su un unicycle che sembra guidare avanti e indietro lungo una curva cubica di Bézier:
L'oggetto SKPaint
utilizzato per troncare sia la mezza pipe che il unicycle è definito come un campo nella UnicycleHalfPipePage
classe . Definito anche è l'oggetto per l'unicycle SKPath
:
public class UnicycleHalfPipePage : ContentPage
{
...
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
SKPath unicyclePath = SKPath.ParseSvgPathData(
"M 0 0" +
"A 25 25 0 0 0 0 -50" +
"A 25 25 0 0 0 0 0 Z" +
"M 0 -25 L 0 -100" +
"A 15 15 0 0 0 0 -130" +
"A 15 15 0 0 0 0 -100 Z" +
"M -25 -85 L 25 -85");
...
}
La classe contiene gli override standard dei metodi e OnDisappearing
per l'animazioneOnAppearing
. Il PaintSurface
gestore crea il percorso per la metà pipe e quindi lo disegna. Viene quindi creato un SKPathMeasure
oggetto in base al percorso seguente:
public class UnicycleHalfPipePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath pipePath = new SKPath())
{
pipePath.MoveTo(50, 50);
pipePath.CubicTo(0, 1.25f * info.Height,
info.Width - 0, 1.25f * info.Height,
info.Width - 50, 50);
canvas.DrawPath(pipePath, strokePaint);
using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
{
float length = pathMeasure.Length;
// Animate t from 0 to 1 every three seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 5 / 5);
// t from 0 to 1 to 0 but slower at beginning and end
t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
SKMatrix matrix;
pathMeasure.GetMatrix(t * length, out matrix,
SKPathMeasureMatrixFlags.GetPositionAndTangent);
canvas.SetMatrix(matrix);
canvas.DrawPath(unicyclePath, strokePaint);
}
}
}
}
Il PaintSurface
gestore calcola un valore compreso t
tra 0 e 1 ogni cinque secondi. Usa quindi la Math.Cos
funzione per convertire tale valore in un valore compreso t
tra 0 e 1 e tornare a 0, dove 0 corrisponde all'uniciclo all'inizio in alto a sinistra, mentre 1 corrisponde all'uniciclo in alto a destra. La funzione coseno fa sì che la velocità sia più lenta nella parte superiore del tubo e più veloce nella parte inferiore.
Si noti che questo valore di t
deve essere moltiplicato per la lunghezza del percorso per il primo argomento in GetMatrix
. La matrice viene quindi applicata all'oggetto SKCanvas
per disegnare il percorso di unicycle.
Enumerazione del percorso
Due classi incorporate di consentono di SKPath
enumerare il contenuto del percorso. Queste classi sono SKPath.Iterator
e SKPath.RawIterator
. Le due classi sono molto simili, ma SKPath.Iterator
possono eliminare gli elementi nel percorso con una lunghezza zero o vicino a una lunghezza zero. Viene RawIterator
usato nell'esempio seguente.
È possibile ottenere un oggetto di tipo SKPath.RawIterator
chiamando il CreateRawIterator
metodo di SKPath
. L'enumerazione tramite il percorso viene eseguita ripetutamente chiamando il Next
metodo . Passarvi una matrice di quattro SKPoint
valori:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
Il Next
metodo restituisce un membro del SKPathVerb
tipo di enumerazione. Questi valori indicano il comando di disegno specifico nel percorso. Il numero di punti validi inseriti nella matrice dipende da questo verbo:
Move
con un singolo puntoLine
con due puntiCubic
con quattro puntiQuad
con tre puntiConic
con tre punti (e chiamare anche ilConicWeight
metodo per il peso)Close
con un puntoDone
Il Done
verbo indica che l'enumerazione del percorso è stata completata.
Si noti che non Arc
esistono verbi. Ciò indica che tutti gli archi vengono convertiti in curve bézier quando vengono aggiunti al percorso.
Alcune informazioni nella SKPoint
matrice sono ridondanti. Ad esempio, se un Move
verbo è seguito da un Line
verbo, il primo dei due punti che accompagnano è Line
lo stesso del Move
punto. In pratica, questa ridondanza è molto utile. Quando si ottiene un Cubic
verbo, è accompagnato da tutti e quattro i punti che definiscono la curva cubica di Bézier. Non è necessario mantenere la posizione corrente stabilita dal verbo precedente.
Il verbo problematico, tuttavia, è Close
. Questo comando disegna una linea retta dalla posizione corrente all'inizio del contorno stabilito in precedenza dal Move
comando. Idealmente, il Close
verbo dovrebbe fornire questi due punti anziché un solo punto. Il peggio è che il punto che accompagna il Close
verbo è sempre (0, 0). Quando si enumera attraverso un percorso, probabilmente sarà necessario mantenere il Move
punto e la posizione corrente.
Enumerazione, appiattimento e formato non valido
A volte è consigliabile applicare una trasformazione algoritmica a un percorso di formato non valido in un modo:
La maggior parte di queste lettere è costituita da linee rette, ma queste linee rette apparentemente sono state ruotate in curve. Com'è possibile?
La chiave è che le linee rette originali sono suddivise in una serie di linee rette più piccole. Queste singole linee rette più piccole possono quindi essere manipolate in modi diversi per formare una curva.
Per semplificare questo processo, l'esempio contiene una classe statica PathExtensions
con un Interpolate
metodo che suddivide una linea retta in numerose linee brevi che sono una sola unità di lunghezza. Inoltre, la classe contiene diversi metodi che converte i tre tipi di curve di Bézier in una serie di piccole linee rette che approssimano la curva. Le formule parametriche sono state presentate nell'articolo Tre tipi di curve di Bézier. Questo processo è detto appiattimento della curva:
static class PathExtensions
{
...
static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
{
int count = (int)Math.Max(1, Length(pt0, pt1));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * pt0.X + t * pt1.X;
float y = (1 - t) * pt0.Y + t * pt1.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
3 * t * (1 - t) * (1 - t) * pt1.X +
3 * t * t * (1 - t) * pt2.X +
t * t * t * pt3.X;
float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
3 * t * (1 - t) * (1 - t) * pt1.Y +
3 * t * t * (1 - t) * pt2.Y +
t * t * t * pt3.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
x /= denominator;
y /= denominator;
points[i] = new SKPoint(x, y);
}
return points;
}
static double Length(SKPoint pt0, SKPoint pt1)
{
return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
}
}
A tutti questi metodi viene fatto riferimento dal metodo CloneWithTransform
di estensione incluso anche in questa classe e illustrato di seguito. Questo metodo clona un percorso enumerando i comandi di percorso e creando un nuovo percorso in base ai dati. Tuttavia, il nuovo percorso è costituito solo da MoveTo
chiamate e LineTo
. Tutte le curve e le linee rette sono ridotte a una serie di linee minuscole.
Quando si chiama CloneWithTransform
, si passa al metodo un Func<SKPoint, SKPoint>
oggetto , che è una funzione con un SKPaint
parametro che restituisce un SKPoint
valore. Questa funzione viene chiamata per ogni punto per applicare una trasformazione algoritmica personalizzata:
static class PathExtensions
{
public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
{
SKPath pathOut = new SKPath();
using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
{
SKPoint[] points = new SKPoint[4];
SKPathVerb pathVerb = SKPathVerb.Move;
SKPoint firstPoint = new SKPoint();
SKPoint lastPoint = new SKPoint();
while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
{
switch (pathVerb)
{
case SKPathVerb.Move:
pathOut.MoveTo(transform(points[0]));
firstPoint = lastPoint = points[0];
break;
case SKPathVerb.Line:
SKPoint[] linePoints = Interpolate(points[0], points[1]);
foreach (SKPoint pt in linePoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[1];
break;
case SKPathVerb.Cubic:
SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
foreach (SKPoint pt in cubicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[3];
break;
case SKPathVerb.Quad:
SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
foreach (SKPoint pt in quadPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Conic:
SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
foreach (SKPoint pt in conicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Close:
SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
foreach (SKPoint pt in closePoints)
{
pathOut.LineTo(transform(pt));
}
firstPoint = lastPoint = new SKPoint(0, 0);
pathOut.Close();
break;
}
}
}
return pathOut;
}
...
}
Poiché il percorso clonato viene ridotto a linee rette minuscole, la funzione di trasformazione ha la possibilità di convertire le linee rette in curve.
Si noti che il metodo mantiene il primo punto di ogni contorno nella variabile denominata firstPoint
e la posizione corrente dopo ogni comando di disegno nella variabile lastPoint
. Queste variabili sono necessarie per costruire la riga di chiusura finale quando viene rilevato un Close
verbo.
L'esempio GlobularText usa questo metodo di estensione per racchiudere apparentemente il testo intorno a un emisfero in un effetto 3D:
Il costruttore della GlobularTextPage
classe esegue questa trasformazione. Crea un SKPaint
oggetto per il testo e quindi ottiene un SKPath
oggetto dal GetTextPath
metodo . Questo è il percorso passato al metodo di estensione insieme a CloneWithTransform
una funzione di trasformazione:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
public GlobularTextPage()
{
Title = "Globular Text";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
using (SKPaint textPaint = new SKPaint())
{
textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
textPaint.TextSize = 100;
using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
{
SKRect textPathBounds;
textPath.GetBounds(out textPathBounds);
globePath = textPath.CloneWithTransform((SKPoint pt) =>
{
double longitude = (Math.PI / textPathBounds.Width) *
(pt.X - textPathBounds.Left) - Math.PI / 2;
double latitude = (Math.PI / textPathBounds.Height) *
(pt.Y - textPathBounds.Top) - Math.PI / 2;
longitude *= 0.75;
latitude *= 0.75;
float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
float y = (float)Math.Sin(latitude);
return new SKPoint(x, y);
});
}
}
}
...
}
La funzione di trasformazione calcola prima due valori denominati longitude
e latitude
che vanno da –π/2 nella parte superiore e sinistra del testo, a π/2 a destra e in basso al testo. L'intervallo di questi valori non è visivamente soddisfacente, quindi vengono ridotti moltiplicando per 0,75. Provare il codice senza apportare tali modifiche. Il testo diventa troppo oscuro ai poli nord e sud, e troppo sottile ai lati. Queste coordinate sferiche tridimensionali vengono convertite in coordinate bidimensionali e y
bidimensionali x
in base alle formule standard.
Il nuovo percorso viene archiviato come campo. Il PaintSurface
gestore deve quindi semplicemente allineare al centro e ridimensionare il percorso per visualizzarlo sullo schermo:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint pathPaint = new SKPaint())
{
pathPaint.Style = SKPaintStyle.Fill;
pathPaint.Color = SKColors.Blue;
pathPaint.StrokeWidth = 3;
pathPaint.IsAntialias = true;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
canvas.DrawPath(globePath, pathPaint);
}
}
}
Questa è una tecnica molto versatile. Se la matrice di effetti di percorso descritti nell'articolo Effetti percorso non comprende abbastanza qualcosa che si ritiene dovrebbe essere incluso, questo è un modo per colmare le lacune.