Effetti percorso in SkiaSharp

Download Sample Scaricare l'esempio

Scopri i vari effetti del percorso che consentono l'uso dei percorsi per la strozzatura e il riempimento

Un effetto percorso è un'istanza della SKPathEffect classe creata con uno dei otto metodi di creazione statici definiti dalla classe . L'oggetto SKPathEffect viene quindi impostato sulla PathEffect proprietà di un SKPaint oggetto per un'ampia gamma di effetti interessanti, ad esempio strozzando una linea con un piccolo percorso replicato:

The Linked Chain sample

Gli effetti del percorso consentono di:

  • Tracciare una linea con punti e trattini
  • Tracciare una linea con qualsiasi percorso riempito
  • Riempire un'area con linee tratteggio
  • Riempire un'area con un percorso affiancato
  • Rendere arrotondati gli angoli taglienti
  • Aggiungere "jitter" casuali alle linee e alle curve

Inoltre, è possibile combinare due o più effetti di percorso.

Questo articolo illustra anche come usare il GetFillPath metodo di SKPaint per convertire un percorso in un altro percorso applicando le proprietà di SKPaint, incluso StrokeWidth e PathEffect. Ciò comporta alcune tecniche interessanti, ad esempio l'acquisizione di un percorso che rappresenta una struttura di un altro percorso. GetFillPath è utile anche in relazione con gli effetti del percorso.

Punti e trattini

L'uso del PathEffect.CreateDash metodo è stato descritto nell'articolo Punti e trattini. Il primo argomento del metodo è una matrice contenente un numero pari di due o più valori, alternando lunghezze di trattini e lunghezze di spazi tra i trattini:

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

Questi valori non sono relativi alla larghezza del tratto. Ad esempio, se la larghezza del tratto è 10 e si desidera una linea composta da trattini quadrati e spazi vuoti quadrati, impostare la intervals matrice su { 10, 10 }. L'argomento phase indica dove inizia il motivo trattino la linea. In questo esempio, se si desidera che la riga inizi con la distanza quadrata, impostare su phase 10.

Le estremità dei trattini sono interessate dalla StrokeCap proprietà di SKPaint. Per larghezze dei tratti, è molto comune impostare questa proprietà su per SKStrokeCap.Round arrotondare le estremità dei trattini. In questo caso, i valori nella intervals matrice non includono la lunghezza aggiuntiva risultante dall'arrotondamento. Questo significa che un punto circolare richiede la specifica di una larghezza pari a zero. Per una larghezza del tratto pari a 10, per creare una linea con punti circolari e spazi tra i punti dello stesso diametro, utilizzare una intervals matrice di { 0, 20 }.

La pagina Testo punteggiato animato è simile alla pagina Testo delineato descritta nell'articolo Integrazione di testo e grafica in che visualizza i caratteri di testo delineati impostando la Style proprietà dell'oggetto SKPaint su SKPaintStyle.Stroke. Inoltre, il testo punteggiato animato usa SKPathEffect.CreateDash per dare a questo contorno un aspetto punteggiato, e il programma anima anche l'argomento phase del SKPathEffect.CreateDash metodo per fare in modo che i punti sembrino spostarsi intorno ai caratteri di testo. Ecco la pagina in modalità orizzontale:

Triple screenshot of the Animated Dotted Text page

La AnimatedDottedTextPage classe inizia definendo alcune costanti ed esegue anche l'override dei OnAppearing metodi e OnDisappearing per l'animazione:

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

Il PaintSurface gestore inizia creando un SKPaint oggetto per visualizzare il testo. La TextSize proprietà viene modificata in base alla larghezza dello schermo:

public class AnimatedDottedTextPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.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;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

Verso la fine del metodo, il SKPathEffect.CreateDash metodo viene chiamato utilizzando l'oggetto dashArray definito come campo e il valore animato phase . L'istanza SKPathEffect di viene impostata sulla PathEffect proprietà dell'oggetto SKPaint per visualizzare il testo.

In alternativa, è possibile impostare l'oggetto sull'oggetto SKPathEffectSKPaint prima di misurare il testo e centrarlo nella pagina. In questo caso, tuttavia, i punti animati e i trattini causano alcune variazioni nella dimensione del testo sottoposto a rendering e il testo tende a vibrare un po '. (Prova!)

Si noterà anche che, come i punti animati intorno ai caratteri di testo, c'è un certo punto in ogni curva chiusa in cui i punti sembrano uscire e uscire dall'esistenza. È qui che inizia e termina il percorso che definisce la struttura del carattere. Se la lunghezza del percorso non è un multiplo integrale della lunghezza del motivo tratteggiato (in questo caso 20 pixel), solo parte di tale motivo può adattarsi alla fine del percorso.

È possibile regolare la lunghezza del motivo trattino in base alla lunghezza del percorso, ma ciò richiede la determinazione della lunghezza del percorso, una tecnica descritta nell'articolo Informazioni sul percorso ed enumerazione.

Il programma Dot/Dash Morph anima il motivo trattino stesso in modo che i trattini sembrino dividere in punti, che si combinano di nuovo per formare trattini:

Triple screenshot of the Dot Dash Morph page

La DotDashMorphPage classe esegue l'override dei OnAppearing metodi e OnDisappearing come il programma precedente, ma la classe definisce l'oggetto SKPaint come campo:

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

Il PaintSurface gestore crea un percorso ellittico in base alle dimensioni della pagina ed esegue una lunga sezione di codice che imposta le dashArray variabili e phase . Poiché la variabile t animata varia da 0 a 1, i if blocchi suddivideranno tale tempo in quattro quarti e, in ognuno di questi trimestri, tsub vanno anche da 0 a 1. Al termine, il programma crea e SKPathEffect lo imposta sull'oggetto per il SKPaint disegno.

Da percorso a percorso

Il GetFillPath metodo di SKPaint trasforma un percorso in un altro in base alle impostazioni nell'oggetto SKPaint . Per vedere come funziona, sostituire la canvas.DrawPath chiamata nel programma precedente con il codice seguente:

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

In questo nuovo codice, la GetFillPath chiamata converte ( ellipsePath che è solo un ovale) in newPath, che viene quindi visualizzata con newPaint. L'oggetto newPaint viene creato con tutte le impostazioni di proprietà predefinite, ad eccezione del fatto che la Style proprietà viene impostata in base al valore restituito booleano da GetFillPath.

Gli oggetti visivi sono identici, ad eccezione del colore, impostato in ellipsePaint ma non newPaint. Anziché l'ellisse semplice definita in ellipsePath, newPath contiene numerosi contorni di percorso che definiscono la serie di punti e trattini. Questo è il risultato dell'applicazione di varie proprietà di ellipsePaint (in particolare, StrokeWidth, StrokeCap, e PathEffect) a ellipsePath e l'inserimento del percorso risultante in newPath. Il GetFillPath metodo restituisce un valore booleano che indica se il percorso di destinazione deve essere compilato. In questo esempio, il valore restituito consiste true nel compilare il percorso.

Provare a modificare l'impostazione Style in newPaintSKPaintStyle.Stroke e verranno visualizzati i singoli contorni del percorso delineati con una linea di larghezza di un pixel.

Strozzamento con un percorso

Il SKPathEffect.Create1DPath metodo è concettualmente simile a SKPathEffect.CreateDash , ad eccezione del fatto che si specifica un percorso anziché un modello di trattini e spazi vuoti. Questo percorso viene replicato più volte per tracciare la linea o la curva.

La sintassi è:

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

In generale, il percorso a cui si passa Create1DPath sarà piccolo e centrato intorno al punto (0, 0). Il advance parametro indica la distanza tra i centri del percorso durante la replica del percorso nella riga. Questo argomento viene in genere impostato sulla larghezza approssimativa del percorso. L'argomento phase svolge lo stesso ruolo nel CreateDash metodo .

Ha SKPath1DPathEffectStyle tre membri:

  • Translate
  • Rotate
  • Morph

Il Translate membro fa sì che il percorso rimanga nello stesso orientamento della replica lungo una linea o una curva. Per Rotate, il percorso viene ruotato in base a una tangente alla curva. Il percorso ha l'orientamento normale per le linee orizzontali. Morph è simile al Rotate fatto che anche il percorso stesso è curvo per corrispondere alla curvatura della linea di cui viene eseguito il tratto.

La pagina 1D Path Effect (Effetto percorso 1D) illustra queste tre opzioni. Il file OneDimensionalPathEffectPage.xaml definisce una selezione contenente tre elementi corrispondenti ai tre membri dell'enumerazione:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

Il file code-behind OneDimensionalPathEffectPage.xaml.cs definisce tre SKPathEffect oggetti come campi. Questi vengono tutti creati usando SKPathEffect.Create1DPath con SKPath gli oggetti creati usando SKPath.ParseSvgPathData. Il primo è una scatola semplice, il secondo è una forma a rombo e il terzo è un rettangolo. Questi vengono usati per illustrare i tre stili di effetto:

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (canvasView != null)
        {
            canvasView.InvalidateSurface();
        }
    }

    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())
        {
            path.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

            canvas.DrawPath(path, pathPaint);
        }
    }
}

Il PaintSurface gestore crea una curva di Bézier che esegue un ciclo intorno a se stessa e accede alla selezione per determinare quale PathEffect deve essere usata per tracciarla. Le tre opzioni , Translate, Rotatee Morph , vengono visualizzate da sinistra a destra:

Triple screenshot of the 1D Path Effect page

Il percorso specificato nel SKPathEffect.Create1DPath metodo viene sempre compilato. Il percorso specificato nel DrawPath metodo viene sempre tracciato se la proprietà PathEffect dell'oggetto SKPaint è impostata su un effetto di percorso 1D. Si noti che l'oggetto pathPaint non ha alcuna Style impostazione, che normalmente viene Fillimpostata su , ma il percorso viene tracciato indipendentemente.

La casella usata nell'esempio Translate è quadrata di 20 pixel e l'argomento advance è impostato su 24. Questa differenza causa un divario tra le caselle quando la linea è approssimativamente orizzontale o verticale, ma le caselle si sovrappongono un po 'quando la linea è diagonale perché la diagonale della casella è di 28,3 pixel.

Anche la forma a rombo nell'esempio Rotate è larga 20 pixel. L'oggetto advance è impostato su 20 in modo che i punti continuino a toccare mentre il diamante viene ruotato insieme alla curvatura della linea.

La forma rettangolo nell'esempio Morph è larga 50 pixel con un'impostazione advance di 55 per fare un piccolo gap tra i rettangoli mentre sono piegati intorno alla curva di Bézier.

Se l'argomento advance è minore delle dimensioni del percorso, i percorsi replicati possono sovrapporsi. Questo può comportare alcuni effetti interessanti. La pagina Catena collegata visualizza una serie di cerchi sovrapposti che sembrano simili a una catena collegata, che si blocca nella forma distintiva di una catenaria:

Triple screenshot of the Linked Chain page

Guarda molto vicino e vedrai che quelli non sono effettivamente cerchi. Ogni collegamento della catena è costituito da due archi, ridimensionati e posizionati in modo che sembrino connettersi con collegamenti adiacenti.

Una catena o un cavo di distribuzione uniforme del peso si blocca sotto forma di catenaria. Un arco costruito sotto forma di catenaria invertita beneficia di una uguale distribuzione di pressione dal peso di un arco. La catenaria ha una descrizione matematica apparentemente semplice:

y = a · cosh(x / a)

Il coseno è la funzione coseno iperbolico. Per x uguale a 0, cosh è zero e y è uguale a. Questo è il centro della catenaria. Come la funzione coseno, si dice che cosh sia anche, il che significa che cosh(–x) è uguale a cosh(x)e i valori aumentano per argomenti positivi o negativi. Questi valori descrivono le curve che formano i lati della catenaria.

Trovare il valore corretto di un oggetto per adattare la catenaria alle dimensioni della pagina del telefono non è un calcolo diretto. Se w e h sono la larghezza e l'altezza di un rettangolo, il valore ottimale di un soddisfa l'equazione seguente:

cosh(w / 2 / a) = 1 + h / a

Il metodo seguente nella LinkedChainPage classe incorpora tale uguaglianza facendo riferimento alle due espressioni a sinistra e a destra del segno di uguale come left e right. Per i valori di piccole dimensioni di un oggetto , left è maggiore rightdi . Per i valori di grandi dimensioni di un oggetto , left è minore di right. Il while ciclo si restringe in su un valore ottimale di un:

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

L'oggetto SKPath per i collegamenti viene creato nel costruttore della classe e l'oggetto risultante SKPathEffect viene quindi impostato sulla PathEffect proprietà dell'oggetto SKPaint archiviato come campo:

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

Il processo principale del gestore consiste nel PaintSurface creare un percorso per la catenaria stessa. Dopo aver determinato l'oggetto ottimale e archiviarlo nella optA variabile, deve anche calcolare un offset dalla parte superiore della finestra. Può quindi accumulare una raccolta di SKPoint valori per la catenaria, trasformarla in un percorso e disegnare il percorso con l'oggetto creato SKPaint in precedenza:

public class LinkedChainPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

Questo programma definisce il percorso usato in Create1DPath per avere il punto (0, 0) nel centro. Questo sembra ragionevole perché il punto (0, 0) del percorso è allineato alla linea o alla curva che sta adornando. Tuttavia, è possibile usare un punto non centrato (0, 0) per alcuni effetti speciali.

La pagina Nastro trasportatore crea un percorso simile a un nastro trasportatore oblong con una curva superiore e inferiore ridimensionata in base alle dimensioni della finestra. Tale percorso viene tracciato con un oggetto semplice SKPaint di colore grigio e largo 20 pixel e quindi tracciato di nuovo con un altro SKPaint oggetto con un SKPathEffect oggetto che fa riferimento a un percorso simile a un piccolo bucket:

Triple screenshot of the Conveyor Belt page

Il punto (0, 0) del percorso del secchio è il punto di manipolazione, quindi quando l'argomento phase è animato, i secchi sembrano ruotare intorno al nastro trasportatore, forse raccogliendo acqua in fondo e scaricandolo nella parte superiore.

La ConveyorBeltPage classe implementa l'animazione con override dei OnAppearing metodi e OnDisappearing . Il percorso del bucket viene definito nel costruttore della pagina:

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

Il codice di creazione del bucket viene completato con due trasformazioni che rendono il bucket un po' più grande e ruotarlo lateralmente. L'applicazione di queste trasformazioni è stata più semplice rispetto alla modifica di tutte le coordinate nel codice precedente.

Il PaintSurface gestore inizia definendo un percorso per il nastro trasportatore stesso. Si tratta semplicemente di una coppia di linee e di una coppia di semi-cerchi disegnati con una linea grigio scuro a 20 pixel:

public class ConveyorBeltPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

La logica per disegnare il nastro trasportatore non funziona in modalità orizzontale.

I bucket devono essere distanziati circa 200 pixel sul nastro trasportatore. Tuttavia, il nastro trasportatore non è probabilmente un multiplo di 200 pixel di lunghezza, il che significa che come l'argomento phase di SKPathEffect.Create1DPath è animato, i bucket verranno inseriti e fuori dall'esistenza.

Per questo motivo, il programma calcola innanzitutto un valore denominato length che corrisponde alla lunghezza del nastro trasportatore. Poiché il nastro trasportatore è costituito da linee rette e semi-cerchi, si tratta di un semplice calcolo. Successivamente, il numero di bucket viene calcolato dividendo length per 200. Viene arrotondato all'intero più vicino e tale numero viene quindi diviso in length. Il risultato è una spaziatura per un numero integrale di bucket. L'argomento phase è semplicemente una frazione di questo.

Da percorso a percorso di nuovo

Nella parte inferiore del DrawSurface gestore in Nastro trasportatore impostare come commento la canvas.DrawPath chiamata e sostituirla con il codice seguente:

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Come nell'esempio precedente di GetFillPath, si noterà che i risultati sono uguali ad eccezione del colore. Dopo l'esecuzione GetFillPathdi , l'oggetto newPath contiene più copie del percorso del bucket, ognuna posizionata nello stesso punto in cui l'animazione le ha posizionate al momento della chiamata.

Tratteggio di un'area

Il SKPathEffect.Create2DLines metodo riempie un'area con linee parallele, spesso chiamate linee tratteggio. Il metodo ha la seguente sintassi:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

L'argomento width specifica la larghezza del tratto delle linee del tratteggio. Il matrix parametro è una combinazione di ridimensionamento e rotazione facoltativa. Il fattore di ridimensionamento indica l'incremento in pixel usato da Skia per spaziare le linee di tratteggio. La separazione tra le righe è il fattore di ridimensionamento meno l'argomento width . Se il fattore di ridimensionamento è minore o uguale al width valore, non ci sarà spazio tra le linee del tratteggio e l'area apparirà riempita. Specificare lo stesso valore per la scalabilità orizzontale e verticale.

Per impostazione predefinita, le linee tratteggio sono orizzontali. Se il matrix parametro contiene la rotazione, le linee del tratteggio vengono ruotate in senso orario.

La pagina Riempimento tratteggio illustra questo effetto di percorso. La HatchFillPage classe definisce tre effetti di percorso come campi, il primo per le linee tratteggio orizzontale con una larghezza di 3 pixel con un fattore di ridimensionamento che indica che sono distanziati da 6 pixel. La separazione tra le righe è quindi di tre pixel. Il secondo effetto di percorso è relativo alle linee tratteggio verticali con una larghezza di sei pixel distanziati da 24 pixel (quindi la separazione è di 18 pixel) e la terza è per le linee di tratteggio diagonali a 12 pixel distanziati da 36 pixel.

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Si noti il metodo matrice Multiply . Poiché i fattori di scala orizzontale e verticale sono gli stessi, l'ordine in cui le matrici di ridimensionamento e rotazione vengono moltiplicate non è rilevante.

Il PaintSurface gestore usa questi tre effetti di percorso con tre colori diversi in combinazione con fillPaint per riempire un rettangolo arrotondato per adattarsi alla pagina. La Style proprietà impostata su fillPaint viene ignorata. Quando l'oggetto SKPaint include un effetto di percorso creato da SKPathEffect.Create2DLine, l'area viene riempita indipendentemente da:

public class HatchFillPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

Se si esaminano attentamente i risultati, si noterà che le linee di tratteggio rosso e blu non sono limitate esattamente al rettangolo arrotondato. Questa è apparentemente una caratteristica del codice Skia sottostante. Se questo è insoddisfacente, viene visualizzato un approccio alternativo per le linee di tratteggio diagonali in verde: il rettangolo arrotondato viene utilizzato come percorso di ritaglio e le linee del tratteggio vengono disegnate nell'intera pagina.

Il PaintSurface gestore termina con una chiamata per tracciare semplicemente il rettangolo arrotondato, in modo da vedere la discrepanza con le linee di tratteggio rosso e blu:

Triple screenshot of the Hatch Fill page

La schermata Android non ha un aspetto simile al seguente: il ridimensionamento dello screenshot ha causato il consolidamento delle linee rosse sottili e degli spazi sottili in linee rosse apparentemente più ampie e spazi più ampi.

Riempimento con un percorso

SKPathEffect.Create2DPath Consente di riempire un'area con un percorso replicato orizzontalmente e verticalmente, in effetti affiancando l'area:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

I SKMatrix fattori di ridimensionamento indicano la spaziatura orizzontale e verticale del percorso replicato. Tuttavia, non è possibile ruotare il percorso usando questo matrix argomento. Se si vuole ruotare il percorso, ruotare il percorso stesso usando il Transform metodo definito da SKPath.

Il percorso replicato è in genere allineato ai bordi sinistro e superiore dello schermo anziché all'area riempita. È possibile eseguire l'override di questo comportamento fornendo fattori di conversione compresi tra 0 e i fattori di ridimensionamento per specificare offset orizzontali e verticali dai lati sinistro e superiore.

La pagina Riempimento riquadro percorso illustra questo effetto percorso. Il percorso utilizzato per collegare l'area è definito come campo nella PathTileFillPage classe . Le coordinate orizzontali e verticali vanno da -40 a 40, il che significa che questo percorso è quadrato di 80 pixel:

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    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.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

PaintSurface Nel gestore, le chiamate impostano SKPathEffect.Create2DPath la spaziatura orizzontale e verticale su 64 per far sovrapporre i riquadri quadrati a 80 pixel. Fortunatamente, il percorso assomiglia a un pezzo di puzzle, meshing ben con piastrelle adiacenti:

Triple screenshot of the Path Tile Fill page

Il ridimensionamento dello screenshot originale causa alcune distorsioni, in particolare sullo schermo Android.

Si noti che questi riquadri appaiono sempre interi e non vengono mai troncati. Nei primi due screenshot non è neanche evidente che l'area riempita è un rettangolo arrotondato. Se vuoi troncare questi riquadri in una determinata area, usa un percorso di ritaglio.

Provare a impostare la Style proprietà dell'oggetto SKPaint su Strokee verranno visualizzati i singoli riquadri descritti anziché riempiti.

È anche possibile riempire un'area con una bitmap affiancata, come illustrato nell'articolo Affiancamento bitmap SkiaSharp.

Arrotondamento angoli taglienti

Il programma Eptagone arrotondato presentato nell'articolo Three Ways to Draw an Arc ha utilizzato un arco tangente per curvare i punti di una figura a sette lati. La pagina Altro heptagon arrotondato mostra un approccio molto più semplice che usa un effetto di percorso creato dal SKPathEffect.CreateCorner metodo :

public static SKPathEffect CreateCorner (Single radius)

Anche se il singolo argomento è denominato radius, è necessario impostarlo sulla metà del raggio dell'angolo desiderato. Si tratta di una caratteristica del codice Skia sottostante.

Ecco il PaintSurface gestore nella AnotherRoundedHeptagonPage classe :

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = new SKPoint[numVertices];
    double vertexAngle = -0.5f * Math.PI;       // straight up

    // Coordinates of the vertices of the polygon
    for (int vertex = 0; vertex < numVertices; vertex++)
    {
        vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                       radius * (float)Math.Sin(vertexAngle));
        vertexAngle += 2 * Math.PI / numVertices;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // Render the path in the center of the screen
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = 10;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

È possibile utilizzare questo effetto con strozzature o riempimento in base alla Style proprietà dell'oggetto SKPaint . Di seguito è in esecuzione:

Triple screenshot of the Another Rounded Heptagon page

Si noterà che questo heptagon arrotondato è identico al programma precedente. Se è necessario più convincente che il raggio dell'angolo sia veramente 100 anziché il 50 specificato nella SKPathEffect.CreateCorner chiamata, è possibile rimuovere il commento dall'istruzione finale nel programma e vedere un cerchio a 100 raggio sovrapposto all'angolo.

Jitter casuale

A volte le linee rette impeccabili della grafica informatica non sono proprio quello che vuoi, e un po 'casualità è desiderato. In tal caso, si vuole provare il SKPathEffect.CreateDiscrete metodo :

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

È possibile usare questo effetto di percorso per la strozzatura o il riempimento. Le linee sono separate in segmenti collegati, ovvero la lunghezza approssimativa della quale è specificata da segLength , e si estendono in direzioni diverse. L'extent della deviazione dalla riga originale viene specificato da deviation.

L'argomento finale è un valore di inizializzazione usato per generare la sequenza pseudo-casuale usata per l'effetto. L'effetto di instabilità avrà un aspetto leggermente diverso per semi diversi. L'argomento ha un valore predefinito pari a zero, il che significa che l'effetto è lo stesso ogni volta che si esegue il programma. Se vuoi un'instabilità diversa ogni volta che la schermata viene ridisegnata, puoi impostare il valore di inizializzazione sulla Millisecond proprietà di un DataTime.Now valore (ad esempio).

La pagina Jitter Experiment (Esperimento jitter) consente di sperimentare con valori diversi in un rettangolo:

Triple screenshot of the JitterExperiment page

Il programma è semplice. Il file JitterExperimentPage.xaml crea un'istanza di due Slider elementi e :SKCanvasView

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

Il PaintSurface gestore nel file code-behind JitterExperimentPage.xaml.cs viene chiamato ogni volta che viene modificato un Slider valore. SKPathEffect.CreateDiscrete Chiama usando i due Slider valori e lo usa per tracciare un rettangolo:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

È possibile utilizzare questo effetto anche per riempire, nel qual caso il contorno dell'area riempita è soggetto a queste deviazioni casuali. La pagina Jitter Text illustra l'uso di questo effetto percorso per visualizzare il testo. La maggior parte del codice nel PaintSurface gestore della JitterTextPage classe è dedicata al dimensionamento e al centro del testo:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.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;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Qui è in esecuzione in modalità orizzontale:

Triple screenshot of the JitterText page

Struttura del percorso

Sono già stati illustrati due esempi di GetFillPath metodo di SKPaint, che esistono due versioni:

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

Sono necessari solo i primi due argomenti. Il metodo accede al percorso a cui fa riferimento l'argomento src , modifica i dati del percorso in base alle proprietà del tratto nell'oggetto SKPaint (inclusa la PathEffect proprietà ) e quindi scrive i risultati nel dst percorso. Il resScale parametro consente di ridurre la precisione per creare un percorso di destinazione più piccolo e l'argomento può eliminare i contorni all'esterno cullRect di un rettangolo.

Un uso di base di questo metodo non comporta alcun effetto di percorso: se l'oggetto SKPaint ha la proprietà Style impostata su SKPaintStyle.Strokee non ne dispone PathEffect, GetFillPath crea un percorso che rappresenta una struttura del percorso di origine come se fosse stato tracciato dalle proprietà di disegno.

Ad esempio, se il src percorso è un cerchio semplice di raggio 500 e l'oggetto SKPaint specifica una larghezza del tratto pari a 100, il dst percorso diventa due cerchi concentrici, uno con un raggio di 450 e l'altro con un raggio di 550. Il metodo viene chiamato GetFillPath perché il riempimento di questo dst percorso è uguale a quello del src percorso. Ma è anche possibile tracciare il dst percorso per visualizzare i contorni del percorso.

Il tocco per strutturare il percorso dimostra questo. E SKCanvasView vengono create un'istanza nel file TapToOutlineThePathPage.xaml.TapGestureRecognizer Il file code-behind TapToOutlineThePathPage.xaml.cs definisce tre SKPaint oggetti come campi, due per la larghezza del tratto di 100 e 20 e il terzo per il riempimento:

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

Se lo schermo non è stato toccato, il PaintSurface gestore usa gli oggetti e redThickStroke disegnare per eseguire il blueFill rendering di un percorso circolare:

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

Il cerchio viene riempito e tratto come ci si aspetta:

Triple screenshot of the normal Tap To Outline The Path page

Quando si tocca lo schermo, outlineThePath viene impostato su truee il PaintSurface gestore crea un nuovo SKPath oggetto e lo usa come percorso di destinazione in una chiamata a GetFillPath sull'oggetto redThickStroke paint. Il percorso di destinazione viene quindi riempito e tracciato con redThinStroke, con il risultato seguente:

Triple screenshot of the outlined Tap To Outline The Path page

I due cerchi rossi indicano chiaramente che il percorso circolare originale è stato convertito in due contorni circolari.

Questo metodo può essere molto utile nello sviluppo di percorsi da usare per il SKPathEffect.Create1DPath metodo . I percorsi specificati in questi metodi vengono sempre compilati quando i percorsi vengono replicati. Se non si vuole riempire l'intero percorso, è necessario definire attentamente i contorni.

Nell'esempio Catena collegata, ad esempio, i collegamenti sono stati definiti con una serie di quattro archi, ognuno dei quali era basato su due raggi per delineare l'area del percorso da riempire. È possibile sostituire il codice nella LinkedChainPage classe per farlo in modo leggermente diverso.

Prima di tutto, è necessario ridefinire la linkRadius costante:

const float linkRadius = 27.5f;
const float linkThickness = 5;

L'oggetto linkPath è ora solo due archi basati su quel singolo raggio, con gli angoli di inizio e gli angoli di sweep desiderati:

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

L'oggetto outlinePath è quindi il destinatario della struttura di linkPath quando viene tracciato con le proprietà specificate in strokePaint.

Un altro esempio di utilizzo di questa tecnica è successivo per il percorso usato in un metodo.

Combinazione degli effetti del percorso

I due metodi di creazione statici finali di SKPathEffect sono SKPathEffect.CreateSum e SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Entrambi questi metodi combinano due effetti di percorso per creare un effetto di percorso composito. Il CreateSum metodo crea un effetto di percorso simile ai due effetti di percorso applicati separatamente, mentre CreateCompose applica un effetto di percorso (l'oggetto inner) e quindi applica a outer tale oggetto.

Si è già visto come il GetFillPath metodo di SKPaint può convertire un percorso in un altro percorso in SKPaint base alle proprietà (incluso PathEffect) in modo da non essere troppo misterioso come un SKPaint oggetto può eseguire tale operazione due volte con i due effetti di percorso specificati nei CreateSum metodi o CreateCompose .

Un uso ovvio di CreateSum consiste nel definire un SKPaint oggetto che riempie un percorso con un effetto di percorso e traccia il percorso con un altro effetto di percorso. Questo è illustrato nell'esempio Cats in Frame , che visualizza una matrice di gatti all'interno di una cornice con bordi scalloped:

Triple screenshot of the Cats In Frame page

La CatsInFramePage classe inizia definendo diversi campi. È possibile riconoscere il primo campo della PathDataCatPage classe dall'articolo SVG Path Data (Dati percorso SVG). Il secondo percorso si basa su una linea e un arco per il modello scallop del frame:

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    SKPath catPath = SKPath.ParseSvgPathData(
        "M 160 140 L 150 50 220 103" +              // Left ear
        "M 320 140 L 330 50 260 103" +              // Right ear
        "M 215 230 L 40 200" +                      // Left whiskers
        "M 215 240 L 40 240" +
        "M 215 250 L 40 280" +
        "M 265 230 L 440 200" +                     // Right whiskers
        "M 265 240 L 440 240" +
        "M 265 250 L 440 280" +
        "M 240 100" +                               // Head
        "A 100 100 0 0 1 240 300" +
        "A 100 100 0 0 1 240 100 Z" +
        "M 180 170" +                               // Left eye
        "A 40 40 0 0 1 220 170" +
        "A 40 40 0 0 1 180 170 Z" +
        "M 300 170" +                               // Right eye
        "A 40 40 0 0 1 260 170" +
        "A 40 40 0 0 1 300 170 Z");

    SKPaint catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

Può catPath essere utilizzato nel SKPathEffect.Create2DPath metodo se la proprietà dell'oggetto SKPaintStyle è impostata su Stroke. Tuttavia, se catPath viene utilizzato direttamente in questo programma, l'intera testa del gatto verrà riempita e i whisker non saranno neanche visibili. (Prova!) È necessario ottenere la struttura di tale percorso e usare tale SKPathEffect.Create2DPath struttura nel metodo .

Il costruttore esegue questo processo. Prima applica due trasformazioni a per catPath spostare il punto (0, 0) al centro e ridurre le dimensioni. GetFillPath ottiene tutti i contorni dei contorni in outlinedCatPathe tale oggetto viene utilizzato nella SKPathEffect.Create2DPath chiamata. I fattori di ridimensionamento nel SKMatrix valore sono leggermente maggiori della dimensione orizzontale e verticale del gatto per fornire un piccolo buffer tra le tessere, mentre i fattori di traslazione sono stati derivati in modo empirico in modo che un gatto completo sia visibile nell'angolo superiore sinistro della cornice:

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

Il costruttore chiama SKPathEffect.Create1DPath quindi la cornice scalloped. Si noti che la larghezza del percorso è di 100 pixel, ma l'avanzamento è di 75 pixel in modo che il percorso replicato si sovrapponga intorno al frame. L'istruzione finale del costruttore chiama SKPathEffect.CreateSum per combinare i due effetti di percorso e impostare il risultato sull'oggetto SKPaint .

Tutto questo lavoro consente al PaintSurface gestore di essere abbastanza semplice. Deve solo definire un rettangolo e disegnarlo usando framePaint:

public class CatsInFramePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

Gli algoritmi dietro gli effetti del percorso causano sempre la visualizzazione dell'intero percorso utilizzato per la visualizzazione di strozzamento o riempimento, che può causare la visualizzazione di alcuni oggetti visivi all'esterno del rettangolo. La ClipRect chiamata precedente alla DrawRect chiamata consente agli oggetti visivi di essere notevolmente più puliti. (Provalo senza ritagliare!)

È comune usare SKPathEffect.CreateCompose per aggiungere qualche instabilità a un altro effetto di percorso. È certamente possibile sperimentare autonomamente, ma di seguito è riportato un esempio leggermente diverso:

Le linee tratteggiate tratteggiate riempiono un'ellisse con linee tratteggiate. La maggior parte del lavoro nella DashedHatchLinesPage classe viene eseguita direttamente nelle definizioni dei campi. Questi campi definiscono un effetto trattino e un effetto tratteggio. Vengono definiti come static perché vengono quindi a cui viene fatto riferimento in una SKPathEffect.CreateCompose chiamata nella SKPaint definizione:

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Il PaintSurface gestore deve contenere solo il sovraccarico standard più una chiamata a DrawOval:

public class DashedHatchLinesPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

Come si è già scoperto, le linee di tratteggio non sono limitate esattamente all'interno dell'area, e in questo esempio iniziano sempre a sinistra con un trattino intero:

Triple screenshot of the Dashed Hatch Lines page

Ora che hai visto effetti di percorso che vanno da semplici punti e trattini a combinazioni strane, usa la tua immaginazione e vedi cosa puoi creare.