Pfadeffekte in SkiaSharp

Beispiel herunterladen Das Beispiel herunterladen

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

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

Beispiel für verknüpfte Kette

Pfadeffekte ermöglichen Folgendes:

  • Streichen einer Linie mit Punkten und Bindestrichen
  • Strich einer Zeile mit einem beliebigen ausgefüllten Pfad
  • Füllen eines Bereichs mit Schraffurlinien
  • Füllen eines Bereichs mit einem kachelnden Pfad
  • Abgerundete Ecken
  • 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 veranschaulicht, wie Sie die GetFillPath -Methode von SKPaint verwenden, um einen Pfad in einen anderen Pfad zu konvertieren, indem Sie die Eigenschaften von SKPaintanwenden, einschließlich StrokeWidth und PathEffect. Dies führt zu einigen interessanten Techniken, z. B. zum Abrufen eines Pfads, der eine Gliederung eines anderen Pfads ist. GetFillPath ist auch im Zusammenhang mit Pfadeffekten hilfreich.

Punkte und Striche

Die Verwendung der PathEffect.CreateDash -Methode wurde im Artikel Punkte und Bindestriche beschrieben. Das erste Argument der -Methode ist ein Array, das eine gerade Anzahl von zwei oder mehr Werten enthält, die zwischen Bindestrichlängen und Lückenlängen zwischen den Bindestrichen wechseln:

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

Diese Werte sind nicht relativ zur Strichbreite. Wenn die Strichbreite beispielsweise 10 beträgt und Sie eine Linie aus quadratischen Bindestrichen und quadratischen Lücken benötigen, legen Sie das intervals Array auf { 10, 10 } fest. Das phase -Argument gibt an, wo innerhalb des Bindestrichmusters die Linie beginnt. Wenn die Linie in diesem Beispiel mit der quadratischen Lücke beginnen soll, legen Sie auf 10 fest phase .

Die Enden der Bindestriche werden von der StrokeCap -Eigenschaft von SKPaintbeeinflusst. Bei breiten Strichbreiten ist es sehr üblich, diese Eigenschaft auf festzulegen, um SKStrokeCap.Round die Enden der Bindestriche zu runden. 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 die Angabe einer Breite von 0 (null) erfordert. Verwenden Sie für eine Strichbreite von 10 ein Array von { 0, 20 }, um eine intervals Linie mit kreisförmigen Punkten und Lücken zwischen den Punkten desselben Durchmessers zu erstellen.

Die Seite Animierter gepunkteter Text ähnelt der seite "Umrissener Text ", die im Artikel Integrieren von Text und Grafiken beschrieben wird, da sie umrissene Textzeichen anzeigt, indem die Style -Eigenschaft des SKPaint -Objekts auf SKPaintStyle.Strokefestgelegt wird. Darüber hinaus verwendet SKPathEffect.CreateDashAnimierter gepunkteter Text, um dieser Gliederung ein gepunktetes Aussehen zu verleihen, und das Programm animiert auch das phase Argument der SKPathEffect.CreateDash -Methode, damit die Punkte scheinbar um die Textzeichen herumzubewegen scheinen. Dies ist die Seite im Querformat:

Dreifacher Screenshot der Seite

Die AnimatedDottedTextPage -Klasse beginnt mit der Definition einiger Konstanten und überschreibt auch die OnAppearing Methoden und OnDisappearing 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 mit dem aufgerufen, das dashArray als Feld definiert ist, und dem animierten phase Wert. Die SKPathEffect instance 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 die animierten Punkte und Bindestriche jedoch eine gewisse Abweichung in der Größe des gerenderten Texts, und der Text neigt dazu, ein wenig zu vibrieren. (Probieren Sie es aus!)

Sie werden auch feststellen, dass, wenn die animierten Punkte um die Textzeichen kreisen, in jeder geschlossenen Kurve einen bestimmten Punkt gibt, an dem die Punkte ein- und aus dem Existieren scheinen. Hier beginnt und endet der Pfad, der die Zeichengliederung definiert. Wenn die Pfadlänge kein integrales Vielfaches der Länge des Bindestrichmusters 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 Bindestrichmusters an die Länge des Pfads anzupassen, aber dafür muss die Länge des Pfads bestimmt werden, eine Technik, die im Artikel Pfadinformationen und Enumeration behandelt wird.

Das Dot/Dash Morph-Programm animiert das Bindestrichmuster selbst so, dass Bindestriche in Punkte zu teilen scheinen, die sich zu Bindestrichen zusammenführen:

Dreifacher Screenshot der Seite

Die DotDashMorphPage -Klasse überschreibt die OnAppearing Methoden und OnDisappearing 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 und phase festlegt. Da die animierte Variable t von 0 bis 1 reicht, teilen sich die if Blöcke diese Zeit in vier Quartale auf und liegen in jedem dieser Quartale tsub ebenfalls zwischen 0 und 1. Ganz am Ende erstellt das Programm die SKPathEffect und legt es auf das SKPaint Objekt zum Zeichnen fest.

Von Pfad zu Pfad

Die GetFillPath -Methode von SKPaint wandelt einen Pfad basierend auf den Einstellungen im -Objekt in SKPaint einen anderen um. 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 (bei dem ellipsePath es sich nur um ein Oval handelt) in newPath, das 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 von GetFillPathfestgelegt wird.

Die visuellen Elemente sind identisch, mit Ausnahme der Farbe, die in ellipsePaint festgelegt ist, aber nicht newPaint. Anstelle der in definierten ellipsePathnewPath einfachen Ellipse enthält zahlreiche Pfadkonturen, die die Reihe von Punkten und Bindestrichen definieren. Dies ist das Ergebnis, wenn verschiedene Eigenschaften von ellipsePaint (insbesondere StrokeWidth, StrokeCapund PathEffect) auf ellipsePath angewendet und der resultierende Pfad in newPathfestgelegt wird. Die GetFillPath -Methode gibt einen booleschen Wert zurück, der angibt, ob der Zielpfad ausgefüllt werden soll. In diesem Beispiel ist true der Rückgabewert für das Füllen des Pfads.

Versuchen Sie, die Style Einstellung in newPaint zu SKPaintStyle.Stroke ändern, und Sie sehen die einzelnen Pfadkonturen, die mit einer Linie mit einer Breite von einem Pixel umrissen sind.

Streicheln mit einem Pfad

Die SKPathEffect.Create1DPath -Methode ähnelt vom Konzept her mit der SKPathEffect.CreateDash Ausnahme, dass Sie einen Pfad anstelle eines Musters von Bindestrichen und Lücken angeben. Dieser Pfad wird mehrmals repliziert, um die Linie oder Kurve zu streichen.

Die Syntax lautet:

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

Im Allgemeinen ist der Pfad, zu Create1DPath dem Sie gehen, klein und um den Punkt (0, 0) zentriert. 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 die gleiche Rolle wie in der CreateDash -Methode.

Die SKPath1DPathEffectStyle hat drei Member:

  • Translate
  • Rotate
  • Morph

Das Translate Element bewirkt, dass der Pfad in derselben Ausrichtung verbleibt, wie er entlang einer Linie oder Kurve repliziert wird. Für Rotatewird der Pfad basierend auf einem Tangens zur Kurve gedreht. Der Pfad hat seine normale Ausrichtung für horizontale Linien. Morph ist ähnlich, Rotate außer dass der Pfad selbst ebenfalls gekrümmt ist, um der Krümmung der zu streichelnden Linie zu entsprechen.

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 CodeBehind-Datei OneDimensionalPathEffectPage.xaml.cs definiert drei SKPathEffect Objekte als Felder. Diese werden alle mit SKPathEffect.Create1DPathSKPath mit -Objekten erstellt, die mit SKPath.ParseSvgPathDataerstellt wurden. Die erste ist eine einfache Box, die zweite ist eine Rautenform, und das dritte ist ein Rechteck. Diese werden verwendet, um die drei Effektstile 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 selbst dreht, und greift auf die Auswahl zu, um zu bestimmen, welche PathEffect zum Streichen 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 gestrichelt, wenn die -Eigenschaft des SKPaint Objekts auf einen 1D-Pfadeffekt festgelegt ist PathEffect . Beachten Sie, dass das pathPaint -Objekt keine Style Einstellung aufweist, die normalerweise standardmäßig auf Fillfestgelegt ist, aber der Pfad wird unabhängig davon strichen.

Das im Translate Beispiel verwendete Feld ist 20 Pixel quadratisch, und das advance Argument ist auf 24 festgelegt. Dieser Unterschied verursacht eine Lücke zwischen den Feldern, wenn die Linie ungefähr horizontal oder vertikal ist, die Felder sich jedoch etwas überlappen, 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. Ist advance auf 20 festgelegt, sodass die Punkte weiterhin berühren, während der Diamant 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 eine kleine Lücke zwischen den Rechtecken zu bilden, während sie um die Bézierkurve gebogen sind.

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 einer verknüpften Kette ähneln, die in der charakteristischen Form einer Oberleitung hängt:

Dreifacher Screenshot der Seite

Schauen Sie genau hin, und Sie werden sehen, dass es sich nicht um Kreise handelt. Jedes Glied in der Kette besteht aus zwei Bögen, die so groß und positioniert sind, dass sie mit angrenzenden Gliedern verbunden zu sein scheinen.

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

y = a · cosh(x / a)

Der Cosh ist die hyperbolische Kosinusfunktion. Für x gleich 0 ist cosh null und y gleich a. Das ist die Mitte der Oberleitung. Wie die Kosinusfunktion soll coshgleichmäßig sein, was bedeutet, dass cosh(–x) gleich cosh(x) ist und die Werte für zunehmende positive oder negative Argumente steigen. Diese Werte beschreiben die Kurven, die die Seiten der Oberleitung bilden.

Die Suche nach dem richtigen Wert von einer , um die Oberleitung 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 eines die folgende Gleichung:

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

Die folgende Methode in der LinkedChainPage -Klasse schließt diese Gleichheit ein, indem sie auf die beiden Ausdrücke links und rechts des Gleichheitszeichens als left und rightverweist. Für kleine Werte von ist größerleft als right; für große Werte ist einleft kleiner als right. Die while Schleife verengt sich auf einen optimalen Wert von:

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 Links wird im Konstruktor der Klasse erstellt, und das resultierende SKPathEffect Objekt wird dann auf die PathEffect Eigenschaft des Objekts festgelegt, das SKPaint als Feld gespeichert ist:

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 Oberleitung selbst zu erstellen. Nachdem Sie das optimale a ermittelt und in der optA Variablen gespeichert haben, muss auch ein Offset von oben im Fenster berechnet werden. Anschließend kann eine Auflistung von SKPoint Werten für die Oberleitung gesammelt, in einen Pfad umgewandelt und der Pfad mit dem zuvor erstellten Objekt gezeichnet werden SKPaint :

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 in Create1DPath verwendet wird, um seinen Punkt (0, 0) in der Mitte zu haben. Dies erscheint sinnvoll, da der Punkt (0, 0) des Pfads an der Linie oder Kurve ausgerichtet ist, die er schmückt. Sie können jedoch einen nicht zentrierten Punkt (0, 0) für einige Spezialeffekte verwenden.

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

Dreifacher Screenshot der Seite

Der Punkt (0, 0) des Löffelpfads ist der Griff. Wenn das phase Argument also animiert ist, scheinen sich die Eimer um das Förderband zu drehen, vielleicht schöpfen sie Wasser unten auf und lassen es oben aus.

Die ConveyorBeltPage -Klasse implementiert eine Animation mit Außerkraftsetzungen der OnAppearing Methoden und OnDisappearing . 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 ihn seitlich drehen. Das Anwenden dieser Transformationen war einfacher als das Anpassen aller Koordinaten im vorherigen Code.

Der PaintSurface Handler definiert zunächst einen Pfad für das Förderband selbst. Dies ist einfach ein Linienpaar und ein Paar von Halbkreisen, 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 Querformatmodus.

Die Löffel sollten etwa 200 Pixel voneinander entfernt auf dem Förderband angeordnet sein. Das Förderband ist jedoch wahrscheinlich kein Vielfaches von 200 Pixel lang, was bedeutet, dass, wenn das phase Argument von SKPathEffect.Create1DPath animiert ist, Buckets in die Existenz und aus der Existenz kommen.

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

Von Pfad zu Pfad erneut

Kommentieren Sie am unteren Rand des DrawSurface Handlers in Conveyor Belt 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 von GetFillPathsehen Sie, 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 der gleichen Stelle positioniert sind, an der die Animation sie zum Zeitpunkt des Aufrufs positioniert hat.

Schraffur eines Bereichs

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

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

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

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

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

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 Rotationsmatrizen multipliziert werden, keine Rolle.

Der PaintSurface Handler verwendet diese drei Pfadeffekte mit drei verschiedenen Farben in Kombination mit fillPaint , um ein abgerundetes Rechteck zu füllen, um die Seite anzupassen. Die Style auf festgelegte fillPaint Eigenschaft wird ignoriert. Wenn das SKPaint Objekt einen Pfadeffekt enthält, der aus erstellt wurde, wird der Bereich unabhängig davon SKPathEffect.Create2DLinegefü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 sich die Ergebnisse genau ansehen, werden Sie feststellen, dass die roten und blauen Schraffurlinien nicht genau auf das abgerundete Rechteck beschränkt sind. (Dies ist anscheinend ein Merkmal des zugrunde liegenden Skia-Codes.) Wenn dies nicht zufriedenstellend ist, wird ein alternativer Ansatz für die diagonalen Schraffurlinien in Grün angezeigt: Das abgerundete Rechteck wird als Abschneidepfad verwendet, und die Schraffurlinien werden auf der gesamten Seite gezeichnet.

Der PaintSurface Handler schließt mit einem Aufruf ab, einfach das abgerundete Rechteck zu streichen, sodass Sie die Diskrepanz mit den roten und blauen Schraffurlinien sehen können:

Dreifacher Screenshot der Seite

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

Füllen mit einem Pfad

Mit SKPathEffect.Create2DPath können Sie einen Bereich mit einem Pfad füllen, der horizontal und vertikal repliziert wird, im Effekt, dass der Bereich kachelt:

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 rotieren. Wenn Der Pfad gedreht werden soll, drehen Sie den Pfad selbst mithilfe der Transform von definierten SKPathMethode.

Der replizierte Pfad wird normalerweise am linken und oberen Rand des Bildschirms ausgerichtet und nicht am gefüllten Bereich. Sie können dieses Verhalten überschreiben, indem Sie Übersetzungsfaktoren zwischen 0 und den Skalierungsfaktoren bereitstellen, um horizontale und vertikale Offsets von der linken und oberen Seite anzugeben.

Auf der Seite "Pfadkachelfüllung" wird dieser Pfadeffekt veranschaulicht. Der Pfad, der zum Kacheln des Bereichs verwendet wird, 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 Weg einem Puzzleteil, das sich gut mit angrenzenden Kacheln einflechtet:

Dreifacher Screenshot der Seite

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

Beachten Sie, dass diese Kacheln immer ganz aussehen und nie abgeschnitten werden. Auf den ersten beiden Screenshots ist nicht einmal ersichtlich, dass es sich bei dem gefüllten Bereich um ein abgerundetes Rechteck handelt. Wenn Sie diese Kacheln auf einen bestimmten Bereich abschneiden möchten, verwenden Sie einen Beschneidungspfad.

Versuchen Sie, die Style -Eigenschaft des SKPaint -Objekts auf festzulegen Stroke, und Sie sehen, dass die einzelnen Kacheln umrissen statt gefüllt sind.

Es ist auch möglich, einen Bereich mit einer gekachelten Bitmap zu füllen, wie im Artikel SkiaSharp Bitmap tiling gezeigt.

Scharfe Ecken runden

Das im Artikel Three Ways to Draw an Arcvorgestellte Programm "Gerundete Heptagon" 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 den Namen hat radius, müssen Sie es auf die Hälfte des gewünschten Eckradius festlegen. (Dies ist ein Merkmal des zugrunde liegenden Skia-Codes.)

Hier sehen Sie den 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 beim Streichen oder Füllen basierend auf der Style -Eigenschaft des SKPaint Objekts verwenden. Hier wird es ausgeführt:

Dreifacher Screenshot der Seite

Sie sehen, dass dieser abgerundete Heptagon mit dem vorherigen Programm identisch ist. Wenn Sie überzeugender benötigen, dass der Eckradius wirklich 100 ist und nicht die SKPathEffect.CreateCorner im Aufruf angegebenen 50, können Sie die Auskommentierung der letzten Anweisung im Programm aufheben und einen 100-Radius-Kreis über der Ecke sehen.

Zufälliger Jitter

Manchmal sind die makellosen geraden Linien der Computergrafik nicht ganz das, was Sie wollen, und ein wenig Zufälligkeit 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 entweder zum Streichen oder Füllen verwenden. Linien werden in verbundene Segmente unterteilt – deren ungefähre Länge durch segLength angegeben wird – und erstrecken sich in verschiedene Richtungen. Der Umfang der Abweichung von der ursprünglichen Zeile wird durch deviationangegeben.

Das letzte Argument ist ein Seed, mit dem die pseudo-zufällige Sequenz generiert wird, die für den Effekt verwendet wird. Der Jitter-Effekt sieht bei verschiedenen Samen etwas anders aus. Das Argument hat den Standardwert 0. Dies bedeutet, dass der Effekt immer gleich ist, wenn Sie das Programm ausführen. Wenn Sie einen anderen Jitter wünschen, wenn der Bildschirm neu gestrichen wird, 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 beim Streichen eines Rechtecks experimentieren:

Dreifacher Screenshot der JitterExperiment-Seite

Das Programm ist einfach. Die Datei JitterExperimentPage.xaml instanziiert zwei Slider Elemente und ein 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>

Der PaintSurface Handler in der CodeBehind-Datei JitterExperimentPage.xaml.cs wird aufgerufen, wenn sich ein Slider Wert ändert. Es ruft SKPathEffect.CreateDiscrete mit den beiden Slider Werten auf und verwendet diese, um ein Rechteck zu streichen:

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 zum Füllen verwenden. In diesem Fall unterliegt der Umriss des gefüllten Bereichs diesen zufälligen Abweichungen. Die Seite Jitter Text veranschaulicht die Verwendung dieses Pfadeffekts zum Anzeigen von Text. Der größte Teil des Codes im PaintSurface Handler der JitterTextPage -Klasse ist der Größen- und Zentrierung 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

Pfadgliederung

Sie haben bereits zwei kleine Beispiele für die GetFillPath Methode von SKPaintgesehen, die zwei Versionen enthält:

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 die Ergebnisse dann in den dst Pfad. Der resScale Parameter ermöglicht es, die Genauigkeit zu reduzieren, um einen kleineren Zielpfad zu erstellen, und das cullRect Argument kann Konturen außerhalb eines Rechtecks beseitigen.

Eine grundlegende Verwendung dieser Methode beinhaltet überhaupt keine Pfadeffekte: Wenn die -Eigenschaft des SKPaint Objekts auf SKPaintStyle.Strokefestgelegt ist Style und nicht festgelegt istPathEffect, wird ein Pfad erstellt, der GetFillPath eine Gliederung des Quellpfads darstellt, als ob es von den Paint-Eigenschaften gezeichnet worden wäre.

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

Dies wird durch Tippen auf den Pfad veranschaulicht. Die SKCanvasView und TapGestureRecognizer werden in der Datei TapToOutlineThePathPage.xaml instanziiert. Die CodeBehind-Datei TapToOutlineThePathPage.xaml.cs definiert drei SKPaint Objekte als Felder, zwei zum Streichen mit Strichbreiten von 100 und 20 und das dritte zum Fü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 der Bildschirm nicht getippt wurde, verwendet der Handler die PaintSurfaceblueFill Objekte und redThickStroke paint, 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 wie erwartet gefüllt und gestrichelt:

Dreifacher Screenshot der normalen Seite Zum Gliederung des Pfads tippen

Wenn Sie auf den Bildschirm tippen, wird auf truefestgelegt, outlineThePath und der PaintSurface Handler erstellt ein neues SKPath Objekt und verwendet dieses als Zielpfad in einem Aufruf GetFillPath von für das redThickStroke Paint-Objekt. Dieser Zielpfad wird dann gefüllt und mit redThinStrokeStrichen strichen, was Folgendes ergibt:

Dreifacher Screenshot der seite

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

Diese Methode kann sehr nützlich sein, um Pfade zu entwickeln, die für die SKPathEffect.Create1DPath -Methode verwendet werden. Die Pfade, die Sie in diesen Methoden angeben, werden immer gefüllt, wenn die Pfade repliziert werden. Wenn Sie nicht möchten, dass der gesamte Pfad ausgefüllt wird, müssen Sie die Gliederungen sorgfältig definieren.

Im Beispiel für verknüpfte Kette wurden die Glieder beispielsweise mit einer Reihe von vier Bögen definiert, von denen jedes Paar auf zwei Radien basierte, um den Bereich des zu füllenden Pfads zu skizzieren. Es ist möglich, den Code in der LinkedChainPage -Klasse zu ersetzen, um dies etwas anders zu tun.

Zunächst sollten Sie die linkRadius Konstante neu definieren:

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

Der linkPath ist jetzt nur noch zwei Bogen, die auf diesem einzelnen Radius basieren, mit den gewünschten Startwinkeln 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 von linkPath , wenn es mit den in strokePaintangegebenen Eigenschaften gezeichnet wird.

Ein weiteres Beispiel, das diese Technik verwendet, wird als Nächstes für den pfad angezeigt, der in einer -Methode verwendet wird.

Kombinieren von Pfadeffekten

Die beiden letzten statischen Erstellungsmethoden von 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 erstellen. Die CreateSum -Methode erstellt einen Pfadeffekt, der den beiden separat angewendeten Pfadeffekten ähnelt, während CreateCompose ein Pfadeffekt (der inner) angewendet wird, und wendet dann den outer auf diesen an.

Sie haben bereits gesehen, wie die GetFillPath Methode von SKPaint basierend auf SKPaint Eigenschaften (einschließlich PathEffect) einen Pfad in einen anderen Pfad konvertieren kann, sodass es nicht allzu geheimnisvoll sein sollte, wie ein SKPaint Objekt diesen Vorgang zweimal mit den beiden Pfadeffekten ausführen kann, die in den CreateSum Methoden oder CreateCompose angegeben sind.

Eine offensichtliche Verwendung von CreateSum besteht darin, ein SKPaint Objekt zu definieren, das einen Pfad mit einem Pfadeffekt füllt und den Pfad mit einem anderen Pfadeffekt stricht. Dies wird im Beispiel "Cats in Frame " veranschaulicht, in dem ein Array von Katzen in einem Frame mit scalloierten Kanten angezeigt wird:

Dreifacher Screenshot der Seite

Die CatsInFramePage -Klasse definiert zunächst mehrere Felder. Möglicherweise erkennen Sie das erste Feld aus der PathDataCatPage -Klasse aus dem Artikel SVG-Pfaddaten . Der zweite Pfad basiert auf einer Linie und einem Bogen für das Jakobsmuschelmuster 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
    };
    ...
}

Kann catPath in der SKPathEffect.Create2DPath -Methode verwendet werden, wenn die SKPaint Objekteigenschaft Style auf Strokefestgelegt ist. Wenn die catPath jedoch direkt in diesem Programm verwendet wird, wird der gesamte Kopf der Katze gefüllt, und die Schnurrköpfe sind 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 ausgeführt. Zuerst werden zwei Transformationen auf angewendet, um catPath den Punkt (0, 0) in die Mitte zu verschieben und ihn in der Größe herunterzuskalieren. GetFillPath ruft alle Konturen der Konturen in outlinedCatPathab, 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 Rahmens 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 scalloped Frame auf. Beachten Sie, dass die Breite des Pfads 100 Pixel beträgt, der Vorlauf jedoch 75 Pixel beträgt, sodass der replizierte Pfad um den Frame herum überlappen wird. Die letzte Anweisung des Konstruktors ruft auf 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, recht einfach zu sein. Es muss nur ein Rechteck definiert und mit gezeichnet werden 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 angezeigt wird, der zum Streichen oder Füllen verwendet wird, was dazu führen kann, dass einige Visuals außerhalb des Rechtecks angezeigt werden. Durch ClipRect den Aufruf vor dem DrawRect Anruf können die Visuals erheblich sauberer sein. (Probieren Sie es aus, ohne auszuschneiden!)

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

Die Gestrichelten Schraffurlinien füllen eine Ellipse mit Geschlüpften Linien, die gestrichelt sind. Die meiste Arbeit in der DashedHatchLinesPage Klasse wird direkt in den Felddefinitionen ausgeführt. Diese Felder definieren einen Bindestricheffekt und einen Schraffureffekt. Sie werden als static definiert, da dann in einem SKPathEffect.CreateCompose Aufruf in der SKPaint Definition auf sie verwiesen wird:

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 plus einen Aufruf von 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 herausgefunden haben, sind die Schraffurlinien nicht genau auf das Innere des Bereichs beschränkt, und in diesem Beispiel beginnen sie immer links mit einem ganzen Bindestrich:

Dreifacher Screenshot der Seite

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