Condividi tramite


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:

Testo incapsulato su 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:

Screenshot triplo della pagina Lunghezza 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:

Screenshot triplo della pagina Unicycle Half-Pipe

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 punto
  • Line con due punti
  • Cubic con quattro punti
  • Quad con tre punti
  • Conic con tre punti (e chiamare anche il ConicWeight metodo per il peso)
  • Close con un punto
  • Done

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:

Testo incapsulato su un emisfero

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:

Screenshot triplo della pagina Testo Globulare

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.