Share via


Pfadeffekte in SkiaSharp

Entdecken Sie die verschiedenen Pfadeffekte, mit denen Pfade zum Streichen und Ausfüllen verwendet werden können

Ein Pfadeffekt ist eine Instanz der Klasse, die SKPathEffect mit einer von acht statischen Erstellungsmethoden erstellt wird, die von der Klasse definiert werden. Das SKPathEffect Objekt wird dann für eine Vielzahl interessanter Effekte auf die PathEffect Eigenschaft eines SKPaint Objekts festgelegt, z. B. beim Streichen einer Linie mit einem kleinen replizierten Pfad:

Beispiel für verknüpfte Kette

Pfadeffekte ermöglichen Folgendes:

  • Strich einer Linie mit Punkten und Strichen
  • Strich einer Linie mit einem beliebigen gefüllten Pfad
  • Füllen eines Bereichs mit Schlupflinien
  • Ausfüllen eines Bereichs mit einem nebeneinander angeordneten Pfad
  • Scharfe Ecken abgerundet
  • Hinzufügen von zufälligen "Jitter" zu Linien und Kurven

Darüber hinaus können Sie zwei oder mehr Pfadeffekte kombinieren.

In diesem Artikel wird auch die Verwendung der GetFillPath Methode zum Konvertieren eines SKPaint Pfads in einen anderen Pfad veranschaulicht, indem Eigenschaften von SKPaint, einschließlich StrokeWidth und PathEffect. Dies führt zu einigen interessanten Techniken, z. B. das Abrufen eines Pfads, der eine Gliederung eines anderen Pfads ist. GetFillPath ist auch in Verbindung mit Pfadeffekten hilfreich.

Punkte und Striche

Die Verwendung der PathEffect.CreateDash Methode wurde im Artikel Dots und Striche beschrieben. Das erste Argument der Methode ist ein Array mit einer geraden Anzahl von zwei oder mehr Werten, die zwischen Strichlängen und Längen von Lücken zwischen den Bindestrichen wechseln:

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

Diese Werte sind nicht relativ zur Strichbreite. Wenn die Strichbreite beispielsweise 10 ist und Sie eine Linie aus quadratischen Strichen und quadratischen Lücken wünschen, legen Sie das intervals Array auf { 10, 10 } fest. Das phase Argument gibt an, wo innerhalb des Strichmusters die Linie beginnt. Wenn die Linie in diesem Beispiel mit dem quadratischen Abstand beginnen soll, legen Sie diese auf 10 fest phase .

Die Enden der Striche werden von der StrokeCap Eigenschaft von SKPaint. Bei breiten Strichbreiten ist es sehr üblich, diese Eigenschaft so festzulegen, dass SKStrokeCap.Round die Enden der Striche gerundet werden. In diesem Fall enthalten die Werte im intervals Array nicht die zusätzliche Länge, die sich aus der Rundung ergibt. Dies bedeutet, dass ein kreisförmiger Punkt eine Breite von Null angibt. Verwenden Sie für eine Strichbreite von 10 eine Linie mit kreisförmigen Punkten und Lücken zwischen den Punkten desselben Durchmessers, und verwenden Sie ein intervals Array von { 0, 20 }.

Die Seite "Animierter Gepunkteter Text" ähnelt der im Artikel "Integrieren von Text und Grafiken" beschriebenen Seite "Gliederungstext", in der sie umrissige Textzeichen anzeigt, indem sie die Style Eigenschaft des SKPaint Objekts auf ".SKPaintStyle.Stroke Darüber hinaus verwendet SKPathEffect.CreateDash animierter gepunkteter Text, um dieser Kontur eine gepunktete Darstellung zu verleihen, und das Programm animiert auch das phase Argument der SKPathEffect.CreateDash Methode, damit die Punkte scheinbar um die Textzeichen herum reisen. Dies ist die Seite im Querformatmodus:

Dreifacher Screenshot der Seite

Die AnimatedDottedTextPage Klasse beginnt mit dem Definieren einiger Konstanten und überschreibt außerdem die OnAppearing und OnDisappearing Methoden für die Animation:

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

Der PaintSurface Handler erstellt zunächst ein SKPaint Objekt zum Anzeigen des Texts. Die TextSize Eigenschaft wird basierend auf der Bildschirmbreite angepasst:

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

Am Ende der Methode wird die SKPathEffect.CreateDash Methode mithilfe des dashArray als Feld definierten Felds und des animierten phase Werts aufgerufen. Die SKPathEffect Instanz wird auf die PathEffect Eigenschaft des SKPaint Objekts festgelegt, um den Text anzuzeigen.

Alternativ können Sie das SKPathEffect Objekt auf das SKPaint Objekt festlegen, bevor Sie den Text messen und auf der Seite zentrieren. In diesem Fall verursachen jedoch die animierten Punkte und Gedankenstriche eine gewisse Variation der Größe des gerenderten Texts, und der Text neigt dazu, ein wenig zu vibrieren. (Probieren Sie es aus!)

Sie werden auch feststellen, dass beim Kreis der animierten Punkte um die Textzeichen ein bestimmter Punkt in jeder geschlossenen Kurve vorhanden ist, an dem die Punkte scheinbar eingeblendet und nicht mehr vorhanden sind. Hier beginnt und endet der Pfad, der die Zeichengliederung definiert. Wenn die Pfadlänge kein integrales Vielfaches der Länge des Strichmusters ist (in diesem Fall 20 Pixel), kann nur ein Teil dieses Musters am Ende des Pfads passen.

Es ist möglich, die Länge des Strichmusters so anzupassen, dass es an die Länge des Pfads angepasst wird. Dies erfordert jedoch die Bestimmung der Länge des Pfads, eine Technik, die im Artikel Pfadinformationen und Enumeration behandelt wird.

Das Dot /Dash Morph-Programm animiert das Strichmuster selbst, sodass Striche scheinbar in Punkte aufgeteilt werden, die zusammen Bindestriche wieder bilden:

Dreifacher Screenshot der Seite

Die DotDashMorphPage Klasse überschreibt die und OnDisappearing methoden OnAppearing genauso wie das vorherige Programm, aber die Klasse definiert das SKPaint Objekt als Feld:

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

Der PaintSurface Handler erstellt einen elliptischen Pfad basierend auf der Größe der Seite und führt einen langen Codeabschnitt aus, der die dashArray Variablen festlegt phase . Da die animierte Variable t von 0 bis 1 reicht, werden die if Blöcke in vier Viertel unterteilt und in jedem dieser Viertel tsub auch von 0 bis 1. Am Ende erstellt das Programm das SKPathEffect Objekt und legt es auf das Objekt für die SKPaint Zeichnung fest.

Von Pfad zu Pfad

Mit der GetFillPath Methode wird SKPaint ein Pfad basierend auf den Einstellungen im SKPaint Objekt in einen anderen umgewandelt. Um zu sehen, wie dies funktioniert, ersetzen Sie den canvas.DrawPath Aufruf im vorherigen Programm durch den folgenden Code:

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 diesem neuen Code konvertiert der GetFillPath Aufruf die ellipsePath (nur ein Oval) in newPath, die dann mit newPaintangezeigt wird. Das newPaint Objekt wird mit allen Standardeigenschafteneinstellungen erstellt, mit der Ausnahme, dass die Style Eigenschaft basierend auf dem booleschen Rückgabewert festgelegt GetFillPathwird.

Die visuellen Elemente sind identisch, mit Ausnahme der Farbe, die in ellipsePaint , aber nicht newPaintfestgelegt ist. Anstelle der einfachen Ellipse, die in ellipsePathdefiniert sind, newPath enthält zahlreiche Pfadkonturen, die die Reihe von Punkten und Strichen definieren. Dies ist das Ergebnis der Anwendung verschiedener Eigenschaften von ellipsePaint (insbesondere StrokeWidth, , StrokeCapund ) auf ellipsePath und PathEffectdas Platzieren des resultierenden Pfads in newPath. Die GetFillPath Methode gibt einen booleschen Wert zurück, der angibt, ob der Zielpfad ausgefüllt werden soll. In diesem Beispiel dient true der Rückgabewert zum Ausfüllen des Pfads.

Versuchen Sie, die Style Einstellung so newPaint zu SKPaintStyle.Stroke ändern, dass die einzelnen Pfadkonturen mit einer Ein-Pixel-Breite-Linie dargestellt werden.

Streichen mit einem Pfad

Die SKPathEffect.Create1DPath Methode ist konzeptuell ähnlich, SKPathEffect.CreateDash mit der Ausnahme, dass Sie einen Pfad anstelle eines Musters von Strichen und Lücken angeben. Dieser Pfad wird mehrmals repliziert, um die Linie oder Kurve zu strichen.

Die Syntax lautet:

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

Im Allgemeinen ist der Pfad, an Create1DPath den Sie übergeben, klein und zentriert um den Punkt (0, 0). Der advance Parameter gibt den Abstand zwischen den Mittelpunkten des Pfads an, während der Pfad in der Zeile repliziert wird. In der Regel legen Sie dieses Argument auf die ungefähre Breite des Pfads fest. Das phase Argument spielt hier dieselbe Rolle wie in der CreateDash Methode.

Das SKPath1DPathEffectStyle hat drei Mitglieder:

  • Translate
  • Rotate
  • Morph

Das Translate Element bewirkt, dass der Pfad wieder Standard in derselben Ausrichtung wie er entlang einer Linie oder Kurve repliziert wird. For Rotate, the path is rotated based on a tangent to the curve. Der Pfad weist seine normale Ausrichtung für horizontale Linien auf. Morph ist vergleichbar mit Rotate der Ausnahme, dass der Pfad selbst auch gekrümmt ist, um der Krümmung der Linie zu entsprechen, die gestrichelt wird.

Auf der Seite "1D-Pfadeffekt " werden diese drei Optionen veranschaulicht. Die Datei OneDimensionalPathEffectPage.xaml definiert eine Auswahl, die drei Elemente enthält, die den drei Membern der Enumeration entsprechen:

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

Die OneDimensionalPathEffectPage.xaml.cs CodeBehind-Datei definiert drei SKPathEffect Objekte als Felder. Diese werden alle mit SKPathEffect.Create1DPathSKPath Objekten erstellt, die mit SKPath.ParseSvgPathData. Die erste ist ein einfaches Feld, die zweite ist eine Rauteform, und das dritte ist ein Rechteck. Diese werden verwendet, um die drei Effektarten zu veranschaulichen:

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

Der PaintSurface Handler erstellt eine Bézierkurve, die sich um sich herum schleift, und greift auf die Auswahl zu, um zu bestimmen, welche PathEffect zum Strich verwendet werden soll. Die drei Optionen - Translate, Rotateund Morph - werden von links nach rechts angezeigt:

Dreifacher Screenshot der Seite

Der in der SKPathEffect.Create1DPath Methode angegebene Pfad wird immer ausgefüllt. Der in der DrawPath Methode angegebene Pfad wird immer strichen, wenn das SKPaint Objekt seine PathEffect Eigenschaft auf einen 1D-Pfadeffekt festgelegt hat. Beachten Sie, dass das pathPaint Objekt keine Style Einstellung aufweist, die in der Regel standardmäßig festgelegt Fillist, der Pfad jedoch unabhängig davon striche.

Das im Translate Beispiel verwendete Feld ist 20 Pixel quadratisch, und das advance Argument wird auf 24 festgelegt. Dieser Unterschied verursacht einen Abstand zwischen den Feldern, wenn die Linie ungefähr horizontal oder vertikal ist, aber die Felder überlappen sich etwas, wenn die Linie diagonal ist, da die Diagonale des Felds 28,3 Pixel beträgt.

Die Rautenform im Rotate Beispiel ist ebenfalls 20 Pixel breit. Die advance Eigenschaft ist auf 20 festgelegt, sodass die Punkte weiterhin berühren, während die Raute zusammen mit der Krümmung der Linie gedreht wird.

Die Rechteckform im Morph Beispiel ist 50 Pixel breit mit einer advance Einstellung von 55, um einen kleinen Abstand zwischen den Rechtecken zu erzielen, während sie um die Bézierkurve gebogen werden.

Wenn das advance Argument kleiner als die Größe des Pfads ist, können sich die replizierten Pfade überlappen. Dies kann zu einigen interessanten Effekten führen. Auf der Seite "Verknüpfte Kette " wird eine Reihe überlappender Kreise angezeigt, die scheinbar einer verknüpften Kette ähneln, die sich in der unverwechselbaren Form einer Katheäre befindet:

Dreifacher Screenshot der Seite

Schauen Sie sehr nahe, und Sie werden sehen, dass diese nicht wirklich Kreise sind. Jede Verbindung in der Kette ist zwei Bogen, größe und positioniert, sodass sie mit angrenzenden Verknüpfungen verbunden scheinen.

Eine Kette oder ein Kabel einer gleichmäßigen Gewichtsverteilung hängt in Form einer Oberleitung. Ein Bogen, der in Form eines invertierten Katheärs gebaut wurde, profitiert von einer gleichen Verteilung des Drucks vom Gewicht eines Bogens. Die Catenary hat eine scheinbar einfache mathematische Beschreibung:

y = a · cosh(x / a)

Der Cosh ist die hyperbolische Kosinusfunktion. Bei x gleich 0 ist Cosh null und y gleich a. Das ist das Zentrum des Katheärs. Wie die Kosinusfunktionwird Cosh als sogar bezeichnet, was bedeutet, dass Cosh(-x) gleich Cosh(x) ist und die Werte steigen, um positive oder negative Argumente zu erhöhen. Diese Werte beschreiben die Kurven, die die Seiten des Katheärs bilden.

Das Auffinden des richtigen Werts einer, um die Catenary an die Abmessungen der Seite des Telefons anzupassen, ist keine direkte Berechnung. Wenn w und h die Breite und Höhe eines Rechtecks sind, erfüllt der optimale Wert einer Formel die folgende Gleichung:

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

Die folgende Methode in der LinkedChainPage Klasse enthält diese Gleichheit, indem sie auf die beiden Ausdrücke links und rechts vom Gleichheitszeichen verweisen als left und right. Bei kleinen Werten eines left Werts ist der Wert größer als right; bei großen Werten eines Werts leftist kleiner als .right Die while Schleife beschränkt sich auf einen optimalen Wert einer:

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

Das SKPath Objekt für die Verknüpfungen wird im Konstruktor der Klasse erstellt, und das resultierende SKPathEffect Objekt wird dann auf die PathEffect Eigenschaft des SKPaint Objekts festgelegt, das als Feld gespeichert wird:

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

Der Standard Auftrag des PaintSurface Handlers besteht darin, einen Pfad für die Catenary selbst zu erstellen. Nachdem Sie das Optimale ermittelt und in der optA Variablen gespeichert haben, muss sie auch einen Offset vom oberen Rand des Fensters berechnen. Anschließend kann sie eine Sammlung von SKPoint Werten für die Catenary ansammeln, dies in einen Pfad umwandeln und den Pfad mit dem zuvor erstellten SKPaint Objekt zeichnen:

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

Dieses Programm definiert den Pfad, der verwendet wird Create1DPath , um seinen (0, 0) Punkt in der Mitte zu haben. Dies scheint vernünftig, da der (0, 0) Punkt des Pfads an der Linie oder Kurve ausgerichtet ist, die es schmückt. Sie können jedoch einen nicht zentrierten (0, 0) Punkt für einige Spezialeffekte verwenden.

Die Förderbandseite erstellt einen Pfad, der einem länglichen Förderband mit einer gekrümmten Ober- und Unterseite entspricht, die den Abmessungen des Fensters entspricht. Dieser Pfad wird mit einem einfachen SKPaint Objekt von 20 Pixel breitem und farbigem Grau strichen und dann erneut mit einem anderen SKPaint Objekt mit einem SKPathEffect Objekt, das auf einen Pfad verweist, der einem kleinen Bucket ähnelt:

Dreifacher Screenshot der Seite

Der (0, 0) Punkt des Bucketpfads ist der Griff, also wenn das phase Argument animiert wird, scheinen sich die Buckets um den Förderband zu drehen, vielleicht das Wasser am unteren Rand abzuschöpfen und es am oberen Rand zu dumpingn.

Die ConveyorBeltPage Klasse implementiert Animationen mit Außerkraftsetzungen der OnAppearing Und OnDisappearing Methoden. Der Pfad für den Bucket wird im Konstruktor der Seite definiert:

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

Der Bucketerstellungscode wird mit zwei Transformationen abgeschlossen, die den Bucket etwas größer machen und es seitwärts drehen. Das Anwenden dieser Transformationen war einfacher als das Anpassen aller Koordinaten im vorherigen Code.

Der PaintSurface Handler beginnt mit der Definition eines Pfads für das Förderband selbst. Dies ist einfach ein Linienpaar und ein Paar halber Kreise, die mit einer 20 Pixel breiten dunkelgrauen Linie gezeichnet werden:

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

Die Logik zum Zeichnen des Förderbands funktioniert nicht im Querformat.

Die Buckets sollten etwa 200 Pixel auseinander auf dem Förderband sein. Der Förderband ist jedoch wahrscheinlich kein Vielfaches von 200 Pixeln lang, was bedeutet, dass, da das phase Argument SKPathEffect.Create1DPath animiert wird, Buckets ins Leben und aus dem Bestehen kommen.

Aus diesem Grund berechnet das Programm zunächst einen Wert, length der die Länge des Förderbands ist. Da der Fördergurt aus geraden Linien und Semikreisen besteht, ist dies eine einfache Berechnung. Als Nächstes wird die Anzahl der Buckets durch Dividieren length durch 200 berechnet. Dies wird auf die nächste ganze Zahl gerundet, und diese Zahl wird dann in lengtheingeteilt. Das Ergebnis ist ein Abstand für eine integrale Anzahl von Buckets. Das phase Argument ist einfach ein Bruchteil davon.

Von Pfad zu Pfad erneut

Kommentieren Sie am unteren Rand des DrawSurface Handlers im Förderband den canvas.DrawPath Aufruf aus, und ersetzen Sie ihn durch den folgenden Code:

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

Wie im vorherigen Beispiel sehen GetFillPathSie, dass die Ergebnisse mit Ausnahme der Farbe identisch sind. Nach dem Ausführen GetFillPathenthält das newPath Objekt mehrere Kopien des Bucketpfads, die jeweils an derselben Stelle positioniert sind, an der die Animation sie zum Zeitpunkt des Aufrufs positioniert hat.

Schlupfen eines Bereichs

Die SKPathEffect.Create2DLines Methode füllt einen Bereich mit parallelen Linien, die häufig als Schlupflinien bezeichnet werden. Die Methode hat folgende Syntax:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

Das width Argument gibt die Strichbreite der Schlupflinien an. Der matrix Parameter ist eine Kombination aus Skalierung und optionaler Drehung. Der Skalierungsfaktor gibt den Pixelinkrement an, den Skia zum Leerzeichen der Schlupflinien verwendet. Die Trennung zwischen den Linien ist der Skalierungsfaktor minus das width Argument. Wenn der Skalierungsfaktor kleiner oder gleich dem width Wert ist, gibt es keinen Abstand zwischen den Schlupflinien, und der Bereich scheint gefüllt zu sein. Geben Sie denselben Wert für die horizontale und vertikale Skalierung an.

Standardmäßig sind Schlupflinien horizontal. Wenn der matrix Parameter Drehung enthält, werden die Schlupflinien im Uhrzeigersinn gedreht.

Auf der Seite "Fülleffekt " wird dieser Pfadeffekt veranschaulicht. Die HatchFillPage Klasse definiert drei Pfadeffekte als Felder, die erste für horizontale Schlupflinien mit einer Breite von 3 Pixeln mit einem Skalierungsfaktor, der angibt, dass sie 6 Pixel voneinander entfernt sind. Die Trennung zwischen den Linien beträgt daher drei Pixel. Der zweite Pfadeffekt ist für vertikale Schlupflinien mit einer Breite von sechs Pixeln mit 24 Pixel Abstand (sodass die Trennung 18 Pixel beträgt), und der dritte ist für diagonale Schlupflinien 12 Pixel breit, 36 Pixel auseinander.

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

Beachten Sie die Matrixmethode Multiply . Da die horizontalen und vertikalen Skalierungsfaktoren identisch sind, spielt die Reihenfolge, in der die Skalierungs- und Drehmatrizen multipliziert werden, keine Rolle.

Der PaintSurface Handler verwendet diese drei Pfadeffekte mit drei verschiedenen Farben in Kombination mit fillPaint dem Ausfüllen eines abgerundeten Rechtecks, um die Seite anzupassen. Die Style festgelegte Eigenschaft fillPaint wird ignoriert. Wenn das SKPaint Objekt einen Pfadeffekt enthält, der erstellt SKPathEffect.Create2DLinewurde, wird der Bereich unabhängig davon gefüllt:

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

Wenn Sie die Ergebnisse sorgfältig betrachten, werden Sie sehen, dass die roten und blauen Schlupflinien nicht genau auf das abgerundete Rechteck beschränkt sind. (Dies ist offenbar ein Merkmal des zugrunde liegenden Skia-Codes.) Wenn dies nicht zufriedenstellend ist, wird für die diagonalen Schlupflinien in Grün ein alternativer Ansatz angezeigt: Das abgerundete Rechteck wird als Beschneidungspfad verwendet, und die Schlupflinien werden auf der gesamten Seite gezeichnet.

Der PaintSurface Handler endet mit einem Aufruf, um einfach das abgerundete Rechteck zu strichen, sodass Sie die Diskrepanz mit den roten und blauen Schlupflinien sehen können:

Dreifacher Screenshot der Seite

Der Android-Bildschirm sieht nicht wirklich so aus: Die Skalierung des Screenshots hat dazu geführt, dass die dünnen roten Linien und dünnen Leerzeichen in scheinbar breiteren roten Linien und breiteren Leerzeichen konsolidiert wurden.

Ausfüllen mit einem Pfad

Auf SKPathEffect.Create2DPath diese Weise können Sie einen Bereich mit einem Pfad ausfüllen, der horizontal und vertikal repliziert wird, wobei die Kachelung des Bereichs wirksam ist:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

Die SKMatrix Skalierungsfaktoren geben den horizontalen und vertikalen Abstand des replizierten Pfads an. Sie können den Pfad jedoch nicht mit diesem matrix Argument drehen. Wenn der Pfad gedreht werden soll, drehen Sie den Pfad selbst mithilfe der Transform von SKPathder definierten Methode.

Der replizierte Pfad wird normalerweise an den linken und oberen Rand des Bildschirms ausgerichtet, anstatt an dem bereich, der gefüllt wird. Sie können dieses Verhalten außer Kraft setzen, indem Sie Übersetzungsfaktoren zwischen 0 und den Skalierungsfaktoren bereitstellen, um horizontale und vertikale Offsets von der linken und oberen Seite anzugeben.

Die Seite "Pfadkachelfüllung " veranschaulicht diesen Pfadeffekt. Der Pfad zum Tiling des Bereichs wird als Feld in der PathTileFillPage Klasse definiert. Die horizontalen und vertikalen Koordinaten reichen von –40 bis 40, was bedeutet, dass dieser Pfad 80 Pixel quadratisch ist:

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 Im Handler legen die SKPathEffect.Create2DPath Aufrufe den horizontalen und vertikalen Abstand auf 64 fest, damit sich die quadratischen Kacheln mit 80 Pixeln überlappen. Glücklicherweise ähnelt der Pfad einem Puzzlestück, das gut mit angrenzenden Kacheln gittert:

Dreifacher Screenshot der Seite

Die Skalierung des ursprünglichen Screenshots verursacht einige Verzerrungen, insbesondere auf dem Android-Bildschirm.

Beachten Sie, dass diese Kacheln immer vollständig angezeigt werden und nie abgeschnitten werden. Auf den ersten beiden Screenshots ist nicht einmal ersichtlich, dass der gefüllte Bereich ein abgerundetes Rechteck ist. Wenn Sie diese Kacheln auf einen bestimmten Bereich abschneiden möchten, verwenden Sie einen Clippingpfad.

Versuchen Sie, die Style Eigenschaft des SKPaint Objekts auf festzulegen Stroke, und sie sehen die einzelnen Kacheln, die nicht ausgefüllt sind.

Es ist auch möglich, einen Bereich mit einer nebeneinander angeordneten Bitmap zu füllen, wie im Artikel SkiaSharp Bitmap-Tiling gezeigt.

Abgerundete scharfe Ecken

Das rundete Heptagon-Programm, das im Artikel "Three Ways to Draw an Arc" vorgestellt wurde, verwendete einen Tangentenbogen, um die Punkte einer siebenseitigen Figur zu kurven. Die Seite "Another Rounded Heptagon " zeigt einen viel einfacheren Ansatz, der einen Pfadeffekt verwendet, der aus der SKPathEffect.CreateCorner Methode erstellt wurde:

public static SKPathEffect CreateCorner (Single radius)

Obwohl das einzelne Argument benannt radiusist, müssen Sie es auf die Hälfte des gewünschten Eckenradius festlegen. (Dies ist ein Merkmal des zugrunde liegenden Skia-Codes.)

Dies ist der PaintSurface Handler in der AnotherRoundedHeptagonPage Klasse:

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

Sie können diesen Effekt entweder mit Streichen oder Ausfüllen basierend auf der Style Eigenschaft des SKPaint Objekts verwenden. Hier wird folgendes ausgeführt:

Dreifacher Screenshot der Seite

Sie werden sehen, dass dieser gerundete Heptagon mit dem früheren Programm identisch ist. Wenn Sie mehr überzeugen müssen, dass der Eckenradius wirklich 100 und nicht die im SKPathEffect.CreateCorner Anruf angegebenen 50 ist, können Sie die Kommentare im Programm aufheben und einen 100-Radius-Kreis überlagert auf der Ecke sehen.

Zufälliger Jitter

Manchmal sind die einwandfreien geraden Linien der Computergrafiken nicht ganz das, was Sie wollen, und ein wenig Zufall ist erwünscht. In diesem Fall sollten Sie die SKPathEffect.CreateDiscrete Methode ausprobieren:

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

Sie können diesen Pfadeffekt zum Streichen oder Ausfüllen verwenden. Linien werden in verbundene Segmente getrennt , die ungefähre Länge, die durch segLength diese angegeben wird, und in unterschiedliche Richtungen erweitert werden. Der Umfang der Abweichung von der ursprünglichen Zeile wird durch deviationangegeben.

Das letzte Argument ist ein Ausgangswert, der zum Generieren der pseudo-zufälligen Sequenz verwendet wird, die für den Effekt verwendet wird. Der Jitter-Effekt sieht für verschiedene Samen etwas anders aus. Das Argument hat einen Standardwert von Null, was bedeutet, dass der Effekt bei jeder Ausführung des Programms identisch ist. Wenn Sie bei jeder Aktualisierung des Bildschirms einen anderen Jitter wünschen, können Sie den Seed auf die Millisecond Eigenschaft eines DataTime.Now Werts festlegen (z. B.).

Auf der Seite "Jitter Experiment " können Sie mit verschiedenen Werten experimentieren, um ein Rechteck zu streichen:

Dreifacher Screenshot der JitterExperiment-Seite

Das Programm ist einfach. Die Datei JitterExperimentPage.xaml instanziiert zwei Slider Elemente und eineSKCanvasView:

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

Der PaintSurface Handler in der JitterExperimentPage.xaml.cs CodeBehind-Datei wird immer aufgerufen, wenn sich ein Slider Wert ändert. Sie ruft SKPathEffect.CreateDiscrete mithilfe der beiden Slider Werte auf und verwendet diese, um ein Rechteck zu strichen:

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

Sie können diesen Effekt auch für das Ausfüllen verwenden, in diesem Fall unterliegt die Kontur des gefüllten Bereichs diesen zufälligen Abweichungen. Die Jitter-Textseite veranschaulicht die Verwendung dieses Pfadeffekts zum Anzeigen von Text. Der großteil des Codes im PaintSurface Handler der JitterTextPage Klasse ist der Größenanpassung und dem Zentrieren des Texts gewidmet:

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

Hier wird es im Querformatmodus ausgeführt:

Dreifacher Screenshot der JitterText-Seite

Pfadausrung

Sie haben bereits zwei kleine Beispiele für die Methode gesehen, von SKPaintder GetFillPath zwei Versionen vorhanden sind:

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

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

Es sind nur die ersten beiden Argumente erforderlich. Die Methode greift auf den Pfad zu, auf den das src Argument verweist, ändert die Pfaddaten basierend auf den Stricheigenschaften im SKPaint Objekt (einschließlich der PathEffect Eigenschaft) und schreibt dann die Ergebnisse in den dst Pfad. Der resScale Parameter ermöglicht die Reduzierung der Genauigkeit, um einen kleineren Zielpfad zu erstellen, und das cullRect Argument kann Konturen außerhalb eines Rechtecks beseitigen.

Eine grundlegende Verwendung dieser Methode umfasst überhaupt keine Pfadeffekte: Wenn das SKPaint Objekt seine Style Eigenschaft festgelegt SKPaintStyle.Strokehat und nicht über seinen PathEffect Satz verfügt, erstellt sie GetFillPath einen Pfad, der eine Gliederung des Quellpfads darstellt, als wäre es durch die Paint-Eigenschaften gezeichnet worden.

Wenn der src Pfad beispielsweise ein einfacher Kreis des Radius 500 ist und das SKPaint Objekt eine Strichbreite von 100 angibt, wird der dst Pfad zu zwei konzentrischen Kreisen, eines mit einem Radius von 450 und dem anderen mit einem Radius von 550. Die Methode wird aufgerufen GetFillPath , da das Ausfüllen dieses dst Pfads mit dem Streichen des src Pfads identisch ist. Sie können aber auch den dst Pfad strichen, um die Pfadgliederungen anzuzeigen.

The Tap to Outline the Path demonstrates this. Die SKCanvasView Und TapGestureRecognizer werden in der Datei TapToOutlineThePathPage.xaml instanziiert. Die TapToOutlineThePathPage.xaml.cs CodeBehind-Datei definiert drei SKPaint Objekte als Felder, zwei zum Streichen mit Strichbreiten von 100 und 20 und das dritte zum Ausfüllen:

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

Wenn auf den Bildschirm nicht getippt wurde, verwendet der PaintSurface Handler die blueFill objekte und redThickStroke zeichnet, um einen Kreispfad zu rendern:

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

Der Kreis wird gefüllt und striche wie erwartet:

Dreifacher Screenshot der normalen Seite

Wenn Sie auf den Bildschirm tippen, outlineThePath wird festgelegt auf true, und der PaintSurface Handler erstellt ein neues SKPath Objekt und verwendet dies als Zielpfad in einem Aufruf GetFillPath für das redThickStroke Paint-Objekt. Dieser Zielpfad wird dann ausgefüllt und mit redThinStrokeStrichen gefüllt, was zu folgendem Ergebnis führt:

Dreifacher Screenshot der umrissenen Seite

Die beiden roten Kreise weisen eindeutig darauf hin, dass der ursprüngliche Kreispfad in zwei kreisförmige Konturen umgewandelt wurde.

Diese Methode kann bei der Entwicklung von Pfaden für die SKPathEffect.Create1DPath Methode sehr nützlich sein. Die in diesen Methoden angegebenen Pfade werden immer ausgefüllt, wenn die Pfade repliziert werden. Wenn der gesamte Pfad nicht ausgefüllt werden soll, müssen Sie die Gliederungen sorgfältig definieren.

Im Beispiel "Verknüpfte Kette" wurden die Verknüpfungen beispielsweise mit einer Reihe von vier Bogen definiert, von denen jedes Paar auf zwei Bogen basiert, um den Bereich des zu füllenden Pfads zu konturieren. Es ist möglich, den Code in der LinkedChainPage Klasse zu ersetzen, um ihn etwas anders zu erledigen.

Zunächst möchten Sie die linkRadius Konstante neu definieren:

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

Dies linkPath sind jetzt nur zwei Bögen, die auf diesem einzelnen Radius basieren, mit den gewünschten Anfangswinkeln und Kehrwinkeln:

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

        }

    }
}

Das outlinePath Objekt ist dann der Empfänger der Gliederung linkPath , wenn es mit den in strokePaintder Angegebenen Eigenschaften striche wird.

Ein weiteres Beispiel für diese Technik wird als Nächstes für den in einer Methode verwendeten Pfad angezeigt.

Kombinieren von Pfadeffekten

Die beiden endgültigen statischen Erstellungsmethoden SKPathEffect sind SKPathEffect.CreateSum und SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Beide Methoden kombinieren zwei Pfadeffekte, um einen zusammengesetzten Pfadeffekt zu erzeugen. Die CreateSum Methode erstellt einen Pfadeffekt, der den beiden Pfadeffekten ähnelt, die separat angewendet werden, während CreateCompose ein Pfadeffekt (das inner) angewendet wird und dann auf outer diese angewendet wird.

Sie haben bereits gesehen, wie die GetFillPath Methode SKPaint einen Pfad basierend auf SKPaint Eigenschaften (einschließlichPathEffect) in einen anderen Pfad konvertieren kann, damit es nicht zu geheimnisvoll sein sollte, wie ein SKPaint Objekt diesen Vorgang zweimal mit den in den CreateSum methoden CreateCompose angegebenen beiden Pfadeffekten ausführen kann.

Eine offensichtliche Verwendung CreateSum besteht darin, ein SKPaint Objekt zu definieren, das einen Pfad mit einem Pfadeffekt ausfüllt und den Pfad mit einem anderen Pfadeffekt strichet. Dies wird im Beispiel "Katzen in Frame " veranschaulicht, in dem ein Array von Katzen innerhalb eines Rahmens mit abgeschirmten Kanten angezeigt wird:

Dreifacher Screenshot der Seite

Die CatsInFramePage Klasse beginnt mit der Definition mehrerer Felder. Möglicherweise erkennen Sie das erste Feld aus der PathDataCatPage Klasse aus dem SVG-Pfaddatenartikel . Der zweite Pfad basiert auf einer Linie und einem Bogen für das Scallopmuster des Rahmens:

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

Dies catPath kann in der SKPathEffect.Create2DPath Methode verwendet werden, wenn die SKPaint Objekteigenschaft Style auf Stroke. Wenn dies catPath jedoch direkt in diesem Programm verwendet wird, wird der gesamte Kopf der Katze gefüllt, und die Whisker werden nicht einmal sichtbar. (Probieren Sie es aus!) Es ist erforderlich, die Gliederung dieses Pfads abzurufen und diese Gliederung in der SKPathEffect.Create2DPath Methode zu verwenden.

Dieser Auftrag wird vom Konstruktor erledigt. Zunächst werden zwei Transformationen angewendet, um catPath den Punkt (0, 0) in die Mitte zu verschieben und in der Größe nach unten zu skalieren. GetFillPath ruft alle Konturen der Konturen in outlinedCatPath, und dieses Objekt wird im SKPathEffect.Create2DPath Aufruf verwendet. Die Skalierungsfaktoren im SKMatrix Wert sind etwas größer als die horizontale und vertikale Größe der Katze, um einen kleinen Puffer zwischen den Kacheln bereitzustellen, während die Übersetzungsfaktoren etwas empirisch abgeleitet wurden, sodass eine vollständige Katze in der oberen linken Ecke des Frames sichtbar ist:

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

Der Konstruktor ruft SKPathEffect.Create1DPath dann den abgeknockten Frame auf. Beachten Sie, dass die Breite des Pfads 100 Pixel beträgt, aber der Fortschritt beträgt 75 Pixel, sodass der replizierte Pfad um den Frame überlappen wird. Die endgültige Anweisung der Konstruktoraufrufe SKPathEffect.CreateSum , um die beiden Pfadeffekte zu kombinieren und das Ergebnis auf das SKPaint Objekt festzulegen.

All diese Arbeit ermöglicht es dem PaintSurface Handler, ganz einfach zu sein. Es muss nur ein Rechteck definieren und mit 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);
    }
}

Die Algorithmen hinter den Pfadeffekten führen immer dazu, dass der gesamte Pfad, der zum Streichen oder Ausfüllen verwendet wird, angezeigt wird, was dazu führen kann, dass einige visuelle Elemente außerhalb des Rechtecks angezeigt werden. Der ClipRect Aufruf vor dem DrawRect Anruf ermöglicht es, die visuellen Elemente erheblich sauber er zu sein. (Probieren Sie es aus, ohne zu schneiden!)

Es ist üblich, SKPathEffect.CreateCompose einige Jitter zu einem anderen Pfadeffekt hinzuzufügen. Sie können sicherlich selbst experimentieren, aber hier ist ein etwas anderes Beispiel:

Die gestrichelten Strichlinien füllen eine Ellipse mit Schlupflinien, die gestrichelt sind. Die meisten Arbeiten in der DashedHatchLinesPage Klasse werden direkt in den Felddefinitionen ausgeführt. Diese Felder definieren einen Gedankenstricheffekt und einen Schlupfeffekt. Sie werden definiert, static da sie dann in einem SKPathEffect.CreateCompose Aufruf in der SKPaint Definition referenziert werden:

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

Der PaintSurface Handler muss nur den Standardaufwand und einen Aufruf enthalten: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);
    }
    ...
}

Wie Sie bereits entdeckt haben, sind die Schlupflinien nicht genau auf das Innere des Bereichs beschränkt, und in diesem Beispiel beginnen sie immer links mit einem ganzen Gedankenstrich:

Dreifacher Screenshot der Seite

Nachdem Sie nun Pfadeffekte gesehen haben, die von einfachen Punkten und Strichen bis hin zu seltsamen Kombinationen reichen, verwenden Sie Ihre Fantasie und sehen Sie, was Sie schaffen können.