Udostępnij za pośrednictwem


Efekty ścieżki w skiaSharp

Odkryj różne efekty ścieżki, które umożliwiają stosowanie ścieżek do stosowania do stosowania na potrzeby głaszania i wypełniania

Efekt ścieżki to wystąpienie SKPathEffect klasy utworzonej przy użyciu jednej z ośmiu statycznych metod tworzenia zdefiniowanych przez klasę. Następnie SKPathEffect obiekt jest ustawiany na PathEffect właściwość SKPaint obiektu dla różnych interesujących efektów, na przykład wystrojenie wiersza z małą zreplikowanymi ścieżkami:

Przykład połączonego łańcucha

Efekty ścieżki umożliwiają:

  • Pociągnięcie linii kropkami i kreskami
  • Pociągnięcie wiersza z dowolną wypełnioną ścieżką
  • Wypełnianie obszaru liniami kreskowymi
  • Wypełnianie obszaru ścieżką kafelków
  • Zaokrąglić ostre rogi
  • Dodawanie losowego "zakłócenia" do linii i krzywych

Ponadto można połączyć co najmniej dwa efekty ścieżki.

W tym artykule pokazano również, jak za pomocą GetFillPath metody SKPaint przekonwertować jedną ścieżkę na inną ścieżkę, stosując właściwości SKPaint, w tym StrokeWidth i PathEffect. Powoduje to kilka interesujących technik, takich jak uzyskanie ścieżki będącej konspektą innej ścieżki. GetFillPath jest również pomocny w połączeniu z efektami ścieżki.

Kropki i kreski

Użycie PathEffect.CreateDash metody zostało opisane w artykule Kropki i kreski. Pierwszym argumentem metody jest tablica zawierająca parzystą liczbę co najmniej dwóch wartości, naprzemiennie między długościami kreski i długościami przerw między kreskami:

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

Te wartości niewzględne względem szerokości pociągnięcia. Jeśli na przykład szerokość pociągnięcia wynosi 10, a linia składa się z kreski kwadratowych i przerw kwadratowych, ustaw tablicę intervals na { 10, 10 }. Argument phase wskazuje, gdzie rozpoczyna się wzór kreski. W tym przykładzie, jeśli chcesz, aby linia zaczynała się od luki kwadratowej, ustaw wartość phase 10.

Na końce kreski mają wpływ StrokeCap właściwość SKPaint. W przypadku szerokości szerokich pociągnięć bardzo często należy ustawić tę właściwość tak, aby zaokrąglić SKStrokeCap.Round końce kreski. W takim przypadku wartości w tablicy intervalsnie zawierają dodatkowej długości wynikającej z zaokrąglania. Oznacza to, że okrągła kropka wymaga określenia szerokości zera. Dla szerokości pociągnięcia 10, aby utworzyć linię z okrągłymi kropkami i przerwami między kropkami o tej samej średnicy, użyj intervals tablicy { 0, 20 }.

Strona Animowana tekst kropkowana jest podobna do strony Tekst konturowy opisany w artykule Integrowanie tekstu i grafiki, w ramach którego wyświetla znaki tekstowe z konturem, ustawiając Style właściwość SKPaint obiektu na SKPaintStyle.Strokewartość . Ponadto animowany tekst kropkowany używa SKPathEffect.CreateDash w celu nadania temu konturowi kropkowanego wyglądu, a program animuje phase argument SKPathEffect.CreateDash metody , aby kropki wydawały się podróżować wokół znaków tekstowych. Oto strona w trybie poziomym:

Potrójny zrzut ekranu przedstawiający stronę Animowana tekst kropkowana

Klasa AnimatedDottedTextPage rozpoczyna się od zdefiniowania pewnych stałych, a także zastępuje OnAppearing metody i OnDisappearing dla animacji:

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

Procedura PaintSurface obsługi rozpoczyna się od utworzenia SKPaint obiektu w celu wyświetlenia tekstu. Właściwość TextSize jest dostosowywana na podstawie szerokości ekranu:

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

Na końcu metody SKPathEffect.CreateDash metoda jest wywoływana przy użyciu dashArray metody zdefiniowanej jako pole i animowanej phase wartości. Wystąpienie SKPathEffect jest ustawione na PathEffect właściwość SKPaint obiektu w celu wyświetlenia tekstu.

Alternatywnie można ustawić SKPathEffect obiekt na SKPaint obiekt przed pomiarem tekstu i wyśrodkowaniem go na stronie. Jednak w takim przypadku animowane kropki i kreski powodują pewne różnice w rozmiarze renderowanego tekstu, a tekst ma tendencję do wibrowania trochę. (Wypróbuj!)

Zauważysz również, że ponieważ animowane kropki krążą wokół znaków tekstowych, istnieje pewien punkt w każdej zamkniętej krzywej, w której kropki wydają się pojawiać i z istnienia. Jest to miejsce, w którym rozpoczyna się i kończy się ścieżka definiująca konspekt znaku. Jeśli długość ścieżki nie jest integralną wielokrotną długością wzorca kreski (w tym przypadku 20 pikseli), tylko część tego wzorca może zmieścić się na końcu ścieżki.

Można dostosować długość wzorca kreski, aby dopasować długość ścieżki, ale wymaga to określenia długości ścieżki, techniki omówionej w artykule Informacje o ścieżce i wyliczenie.

Program Dot / Dash Morph animuje wzorzec kreski, tak aby kreski wydawały się dzielić na kropki, które łączą się w celu utworzenia kreski ponownie:

Potrójny zrzut ekranu przedstawiający stronę Dot Dash Morph

Klasa DotDashMorphPage zastępuje OnAppearing metody i OnDisappearing tak samo jak poprzedni program, ale klasa definiuje SKPaint obiekt jako pole:

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

Procedura PaintSurface obsługi tworzy wielokropkową ścieżkę na podstawie rozmiaru strony i wykonuje długą sekcję kodu, która ustawia dashArray zmienne i phase . Ponieważ animowana zmienna t waha się od 0 do 1, if bloki dzielą ten czas na cztery czwarte, a w każdym z tych kwartałów tsub również wahają się od 0 do 1. Na samym końcu program tworzy SKPathEffect obiekt i ustawia go na SKPaint obiekt do rysowania.

Od ścieżki do ścieżki

GetFillPath Metoda SKPaint zamienia jedną ścieżkę na inną na podstawie ustawień w SKPaint obiekcie. Aby zobaczyć, jak to działa, zastąp canvas.DrawPath wywołanie w poprzednim programie następującym kodem:

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

W tym nowym kodzie GetFillPath wywołanie konwertuje ellipsePath element (który jest tylko owalnym) na newPathwartość , która jest następnie wyświetlana za pomocą newPaintpolecenia . Obiekt newPaint jest tworzony ze wszystkimi domyślnymi ustawieniami właściwości, z wyjątkiem tego, że Style właściwość jest ustawiona na podstawie wartości zwracanej przez wartość logiczną z GetFillPath.

Wizualizacje są identyczne z wyjątkiem koloru, który jest ustawiony, ellipsePaint ale nie newPaint. Zamiast prostego wielokropka zdefiniowanego w programie ellipsePath, newPath zawiera wiele konturów ścieżek, które definiują serię kropek i kresek. Jest to wynik zastosowania różnych właściwości ellipsePaint (w szczególności , StrokeWidth, StrokeCapi PathEffect) do ellipsePath i umieszczenie wynikowej ścieżki w .newPath Metoda GetFillPath zwraca wartość logiczną wskazującą, czy ścieżka docelowa ma zostać wypełniona. W tym przykładzie wartość zwracana jest true do wypełnienia ścieżki.

Spróbuj zmienić ustawienie na StylenewPaintSKPaintStyle.Stroke i zobaczysz poszczególne kontury ścieżki nakreślane linią o szerokości jednego piksela.

Wystrojanie ze ścieżką

Metoda SKPathEffect.Create1DPath jest koncepcyjnie podobna do SKPathEffect.CreateDash tej, z tą różnicą, że określasz ścieżkę, a nie wzorzec kreski i przerw. Ta ścieżka jest replikowana wiele razy w celu pociągnięcia linii lub krzywej.

Składnia jest następująca:

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

Ogólnie rzecz biorąc, ścieżka przekazywana Create1DPath będzie mała i wyśrodkowana wokół punktu (0, 0). Parametr advance wskazuje odległość między centrami ścieżki, ponieważ ścieżka jest replikowana w wierszu. Zazwyczaj ten argument jest ustawiany na przybliżoną szerokość ścieżki. Argument phase odgrywa tutaj tę samą rolę, co w metodzie CreateDash .

Element SKPath1DPathEffectStyle ma trzy elementy członkowskie:

  • Translate
  • Rotate
  • Morph

Element Translate członkowski powoduje, że ścieżka pozostaje w tej samej orientacji, co jest replikowana wzdłuż linii lub krzywej. W przypadku Rotateparametru ścieżka jest obracana na podstawie tangensu do krzywej. Ścieżka ma normalną orientację dla linii poziomych. Morph jest podobny do Rotate tego, że sama ścieżka jest również zakrzywiona, aby dopasować krzywiznę linii pociągniętej.

Na stronie Efekt ścieżki 1D przedstawiono te trzy opcje. Plik OneDimensionalPathEffectPage.xaml definiuje selektor zawierający trzy elementy odpowiadające trzem członkom wyliczenia:

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

Plik OneDimensionalPathEffectPage.xaml.cs za pomocą kodu definiuje trzy SKPathEffect obiekty jako pola. Wszystkie te obiekty są tworzone przy użyciu SKPathEffect.Create1DPathSKPath obiektów utworzonych przy użyciu polecenia SKPath.ParseSvgPathData. Pierwszy to proste pole, drugi to kształt rombu, a trzeci to prostokąt. Są one używane do zademonstrowania trzech stylów efektu:

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

Procedura PaintSurface obsługi tworzy krzywą Bézier, która krąży wokół siebie i uzyskuje dostęp do selektora w celu określenia, które PathEffect należy użyć do pociągnięcia. Trzy opcje — Translate, Rotatei Morph — są wyświetlane od lewej do prawej:

Potrójny zrzut ekranu przedstawiający stronę efektu ścieżki 1D

Ścieżka określona w metodzie SKPathEffect.Create1DPath jest zawsze wypełniona. Ścieżka określona w DrawPath metodzie jest zawsze pociągnięta, jeśli SKPaint obiekt ma właściwość PathEffect ustawioną na efekt ścieżki 1D. Zwróć uwagę, że pathPaint obiekt nie Style ma żadnego ustawienia, które zwykle jest domyślnie ustawione na Fill, ale ścieżka jest pociągnięta niezależnie od tego.

Pole użyte w przykładzie Translate to 20 pikseli kwadratowych, a advance argument jest ustawiony na 24. Ta różnica powoduje różnicę między polami, gdy linia jest w przybliżeniu pozioma lub pionowa, ale pola nakładają się trochę, gdy linia jest po przekątnej, ponieważ przekątna pola wynosi 28,3 pikseli.

Kształt rombu w przykładzie Rotate ma również szerokość 20 pikseli. Wartość advance jest ustawiona na 20, aby punkty nadal dotykały się, gdy diament jest obracany wraz z krzywizną linii.

Kształt prostokąta w przykładzie Morph ma szerokość 50 pikseli z ustawieniem advance 55, aby zrobić małą lukę między prostokątami, ponieważ są one wygięte wokół krzywej Bézier.

advance Jeśli argument jest mniejszy niż rozmiar ścieżki, replikowane ścieżki mogą się nakładać. Może to spowodować kilka interesujących efektów. Na stronie Połączony łańcuch jest wyświetlana seria nakładających się okręgów, które wydają się przypominać połączony łańcuch, który zawiesza się w charakterystycznym kształcie kategory:

Potrójny zrzut ekranu przedstawiający stronę Połączono łańcuch

Spójrz bardzo blisko i zobaczysz, że nie są to okręgi. Każde łącze w łańcuchu to dwa łuki o rozmiarze i położeniu, więc wydają się łączyć się z połączonymi łączami.

Łańcuch lub kabel równomiernego rozkładu masy zawiesza się w postaci kategoryczny. Łuk zbudowany w postaci odwróconej kategorii korzyści z równego rozkładu ciśnienia z wagi łuku. Kategoryczny ma pozornie prosty opis matematyczny:

y = a · cosh(x / a)

Cosh jest funkcją cosinus hiperboliczną. W przypadku wartości x równej 0 cosh ma wartość zero, a wartość y jest równa a. To jest środek kategoryczny. Podobnie jak funkcja cosinus, cosh mówi się, że jest równomierny, cosh(-x) równa cosh(x), a wartości zwiększają się w celu zwiększenia dodatnich lub ujemnych argumentów. Te wartości opisują krzywe, które tworzą boki kategoryczny.

Znalezienie właściwej wartości elementu , aby dopasować kategorię do wymiarów strony telefonu, nie jest bezpośrednim obliczeniem. Jeśli w i h są szerokością i wysokością prostokąta, optymalna wartość równania spełnia następujące równanie:

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

Poniższa metoda w LinkedChainPage klasie zawiera tę równość, odwołując się do dwóch wyrażeń po lewej i prawej stronie znaku równości jako i leftright. W przypadku małych wartości aleft wartość jest większa niż right; w przypadku dużych wartości aleft wartość jest mniejsza niż right. Pętla while zawęża się na optymalnej wartości:

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

SKPath Obiekt łączy jest tworzony w konstruktorze klasy, a wynikowy SKPathEffect obiekt jest następnie ustawiany na PathEffect właściwość SKPaint obiektu, który jest przechowywany jako pole:

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

Głównym zadaniem PaintSurface programu obsługi jest utworzenie ścieżki dla samej kategory. Po określeniu optymalnego elementu i zapisaniu go w optA zmiennej należy również obliczyć przesunięcie od góry okna. Następnie może zgromadzić kolekcję SKPoint wartości dla kategorii, przekształcić ją w ścieżkę i narysować ścieżkę z wcześniej utworzonym SKPaint obiektem:

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

Ten program definiuje ścieżkę używaną do Create1DPath stosowania punktu (0, 0) w środku. Wydaje się to uzasadnione, ponieważ punkt (0, 0) ścieżki jest wyrównany do linii lub krzywej, że jest to adorowanie. Można jednak użyć punktu nieśrodkowanego (0, 0) dla niektórych efektów specjalnych.

Strona Przenośnik taśmowy tworzy ścieżkę przypominającą podłużny przenośnik taśmowy o zakrzywionym górnym i dolnym rogu, który jest wielkości wymiarów okna. Ta ścieżka jest pociągnięta prostym SKPaint obiektem o szerokości 20 pikseli i kolorze szarym, a następnie pociągniętą ponownie innym SKPaint obiektem z SKPathEffect obiektem odwołującym się do ścieżki przypominającej mały zasobnik:

Potrójny zrzut ekranu przedstawiający stronę przenośnika taśmowego

(0, 0) punkt ścieżki zasobnika jest uchwytem, więc gdy phase argument jest animowany, wiadra wydają się obracać się wokół przenośnika taśmowego, być może zgarniając wodę na dole i wyrzucając go na górze.

Klasa ConveyorBeltPage implementuje animację za pomocą przesłonięć OnAppearing metod i OnDisappearing . Ścieżka zasobnika jest zdefiniowana w konstruktorze strony:

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

Kod tworzenia zasobnika kończy się dwoma przekształceniami, które sprawiają, że zasobnik jest nieco większy i odwraca go w bok. Zastosowanie tych przekształceń było łatwiejsze niż dostosowanie wszystkich współrzędnych w poprzednim kodzie.

Procedura PaintSurface obsługi rozpoczyna się od zdefiniowania ścieżki dla samego przenośnika taśmowego. Jest to po prostu para linii i para półkoli, które są rysowane z 20-pikselową linią ciemnoszary:

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

Logika rysowania przenośnika taśmowego nie działa w trybie poziomym.

Zasobniki powinny być rozmieszczone na taśmie przenośnika o powierzchni około 200 pikseli. Jednak przenośnik taśmowy prawdopodobnie nie jest wielokrotny o długości 200 pikseli, co oznacza, że jako phase argument SKPathEffect.Create1DPath jest animowany, zasobniki wyskakują do i z istnienia.

Z tego powodu program najpierw oblicza wartość o nazwie length , która jest długością przenośnika taśmowego. Ponieważ przenośnik taśmowy składa się z linii prostych i półkoli, jest to proste obliczenie. Następnie liczba zasobników jest obliczana przez podzielenie length przez 200. Jest to zaokrąglane do najbliższej liczby całkowitej, a ta liczba jest następnie podzielona na length. Wynik jest odstępem dla całkowitej liczby zasobników. Argument phase jest po prostu ułamkiem tego.

Od ścieżki do ścieżki ponownie

W dolnej części programu obsługi w przenośniku DrawSurface taśmowym oznacz wywołanie i zastąp canvas.DrawPath go następującym kodem:

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

Podobnie jak w poprzednim przykładzie polecenia GetFillPath, zobaczysz, że wyniki są takie same, z wyjątkiem koloru. Po wykonaniu GetFillPathnewPath operacji obiekt zawiera wiele kopii ścieżki zasobnika, z których każda znajduje się w tym samym miejscu, w którym animacja umieściła je w momencie wywołania.

Wyklucie obszaru

Metoda SKPathEffect.Create2DLines wypełnia obszar liniami równoległymi, często nazywanymi liniami kreskowymi. Metoda ma następującą składnię:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

Argument width określa szerokość pociągnięcia linii kreskowania. Parametr matrix jest kombinacją skalowania i opcjonalnej rotacji. Współczynnik skalowania wskazuje przyrost pikseli używany przez Skię do odstępowania linii kreskowania. Separacja między liniami jest czynnikiem skalowania minus argumentem width . Jeśli współczynnik skalowania jest mniejszy lub równy width wartości, nie będzie spacji między liniami kreskowania, a obszar zostanie wypełniony. Określ tę samą wartość dla skalowania w poziomie i w pionie.

Domyślnie linie kreskowania są poziome. matrix Jeśli parametr zawiera rotację, linie kreskowania są obracane zgodnie z ruchem wskazówek zegara.

Na stronie Wypełnienie kreskowe pokazano ten efekt ścieżki. Klasa HatchFillPage definiuje trzy efekty ścieżki jako pola, pierwszy dla linii kreskowania poziomego o szerokości 3 pikseli ze współczynnikiem skalowania wskazującym, że są rozmieszczone 6 pikseli. Rozdzielenie między liniami wynosi zatem trzy piksele. Drugi efekt ścieżki dotyczy linii kreskowania pionowego o szerokości sześciu pikseli rozmieszczonych 24 pikseli (więc separacja wynosi 18 pikseli), a trzecia dotyczy linii kreskowania ukośnego 12 pikseli o szerokości 36 pikseli.

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

Zwróć uwagę na metodę macierzy Multiply . Ponieważ czynniki skalowania poziomego i pionowego są takie same, kolejność, w jakiej pomnożone są macierze skalowania i obrotu, nie ma znaczenia.

Procedura PaintSurface obsługi używa tych trzech efektów ścieżki z trzema różnymi kolorami w połączeniu z fillPaint , aby wypełnić zaokrąglony rozmiar prostokąta, aby dopasować stronę. Właściwość ustawiona na fillPaint jest ignorowana. Gdy StyleSKPaint obiekt zawiera efekt ścieżki utworzony na podstawie SKPathEffect.Create2DLineelementu , obszar jest wypełniany niezależnie od tego:

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

Jeśli przyjrzysz się dokładnie wynikam, zobaczysz, że czerwone i niebieskie linie kreskowania nie są dokładnie ograniczone do zaokrąglonego prostokąta. (Jest to najwyraźniej cecha bazowego kodu Skia). Jeśli jest to niezadowalająca, alternatywne podejście jest wyświetlane dla ukośnych linii kreskowania w kolorze zielonym: Zaokrąglony prostokąt jest używany jako ścieżka wycinków, a linie kreskowania są rysowane na całej stronie.

Procedura PaintSurface obsługi kończy się wywołaniem prostego pociągnięcia zaokrąglonego prostokąta, dzięki czemu można zobaczyć rozbieżność z czerwonymi i niebieskimi liniami kreskowania:

Zrzut ekranu przedstawiający stronę Wypełnienie kreskowe

Ekran systemu Android tak naprawdę nie wygląda następująco: Skalowanie zrzutu ekranu spowodowało cienkie czerwone linie i cienkie przestrzenie, aby skonsolidować się w pozornie szersze czerwone linie i szersze przestrzenie.

Wypełnianie ścieżką

Obiekt SKPathEffect.Create2DPath umożliwia wypełnienie obszaru ścieżką, która jest replikowana w poziomie i w pionie, w efekcie układając obszar:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

Czynniki skalowania SKMatrix wskazują odstępy w poziomie i w pionie dla replikowanej ścieżki. Ale nie można obrócić ścieżki przy użyciu tego matrix argumentu. Jeśli chcesz, aby ścieżka została obrócona, obróć samą ścieżkę przy użyciu metody zdefiniowanej Transform przez SKPath.

Replikowana ścieżka jest zwykle wyrównana do lewej i górnej krawędzi ekranu, a nie z wypełnionym obszarem. To zachowanie można zastąpić, udostępniając czynniki tłumaczenia między 0 a czynnikami skalowania w celu określenia przesunięć poziomych i pionowych z lewej i górnej strony.

Strona Wypełnienie kafelka ścieżki pokazuje ten efekt ścieżki. Ścieżka używana do układania obszaru jest definiowana jako pole w PathTileFillPage klasie . Współrzędne poziome i pionowe wahają się od –40 do 40, co oznacza, że ta ścieżka to 80 pikseli kwadratowych:

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

W procedurze PaintSurface obsługi SKPathEffect.Create2DPath wywołania ustawia odstępy poziome i pionowe na 64, aby spowodować nakładanie się 80-pikselowych kafelków kwadratowych. Na szczęście ścieżka przypomina kawałek układanki, zazębiając się ładnie z przylegających kafelków:

Potrójny zrzut ekranu przedstawiający stronę Wypełnianie kafelka ścieżki

Skalowanie z oryginalnego zrzutu ekranu powoduje pewne zakłócenia, szczególnie na ekranie systemu Android.

Zwróć uwagę, że te kafelki zawsze pojawiają się całe i nigdy nie są obcięte. Na dwóch pierwszych zrzutach ekranu nie widać nawet, że wypełniony obszar jest zaokrąglonym prostokątem. Jeśli chcesz obciąć te kafelki do określonego obszaru, użyj ścieżki przycinania.

Spróbuj ustawić Style właściwość SKPaint obiektu na Stroke, a poszczególne kafelki zostaną wyświetlone, a nie wypełnione.

Można również wypełnić obszar kafelkiem map bitowych, jak pokazano w artykule SkiaSharp tiling mapy bitowej.

Zaokrąglone ostre rogi

Zaokrąglony program Heptagon przedstawiony w artykule Three Ways to Draw an Arc użył łuku tangensowego, aby zakrzywić punkty siedmiostronnej postaci. Strona Another Rounded Heptagon pokazuje znacznie łatwiejsze podejście, które używa efektu ścieżki utworzonego na SKPathEffect.CreateCorner podstawie metody:

public static SKPathEffect CreateCorner (Single radius)

Mimo że pojedynczy argument ma nazwę radius, należy ustawić go na połowę żądanego promienia rogu. (Jest to cecha bazowego kodu Skia).

PaintSurface Oto procedura obsługi w AnotherRoundedHeptagonPage klasie:

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

Możesz użyć tego efektu za pomocą naciśnięcia lub wypełnienia na Style podstawie właściwości SKPaint obiektu. W tym miejscu działa:

Potrójny zrzut ekranu przedstawiający stronę Innego zaokrąglonego szepta

Zobaczysz, że ten zaokrąglony heptagon jest identyczny z wcześniejszym programem. Jeśli potrzebujesz bardziej przekonującego, że promień rogu jest naprawdę 100, a nie 50 określony w SKPathEffect.CreateCorner wywołaniu, możesz usunąć komentarz końcowy instrukcji w programie i zobaczyć 100-promieniowy okrąg nałożony na rogu.

Losowe zakłócenia

Czasami bezbłędne proste linie grafiki komputerowej nie są do końca potrzebne, a mała losowość jest wymagana. W takim przypadku należy wypróbować metodę SKPathEffect.CreateDiscrete :

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

Możesz użyć tego efektu ścieżki do nałogowania lub wypełnienia. Linie są oddzielone od połączonych segmentów — przybliżona długość określonego przez segLength — i rozszerzają się w różnych kierunkach. Zakres odchylenia od oryginalnej linii jest określony przez deviation.

Ostatnim argumentem jest inicjator używany do generowania pseudo-losowej sekwencji używanej do efektu. Efekt roztrzasku będzie wyglądać nieco inaczej dla różnych nasion. Argument ma wartość domyślną zero, co oznacza, że efekt jest taki sam przy każdym uruchomieniu programu. Jeśli chcesz różnić zakłócenia za każdym razem, gdy ekran zostanie przemalowany, możesz ustawić inicjator na Millisecond właściwość DataTime.Now wartości (na przykład).

Strona Jitter Experiment (Eksperyment jitter) umożliwia eksperymentowanie z różnymi wartościami w przypadku strojania prostokąta:

Zrzut ekranu przedstawiający stronę JitterExperiment

Program jest prosty. Plik JitterExperimentPage.xaml tworzy wystąpienie dwóch Slider elementów i :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>

Program PaintSurface obsługi w pliku JitterExperimentPage.xaml.cs jest wywoływany za każdym razem, gdy Slider zmienia się wartość. Wywołuje SKPathEffect.CreateDiscrete ona użycie dwóch Slider wartości i używa jej do pociągnięcia prostokąta:

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

Można również użyć tego efektu do wypełnienia, w takim przypadku kontur wypełnionego obszaru podlega tym losowym odchyleniom. Strona Jitter Text pokazuje użycie tego efektu ścieżki do wyświetlania tekstu. Większość kodu w procedurze PaintSurface obsługi JitterTextPage klasy jest poświęcona określaniu rozmiaru i wyśrodkowaniu tekstu:

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

W tym miejscu działa w trybie poziomym:

Potrójny zrzut ekranu przedstawiający stronę JitterText

Konspektowanie ścieżki

Znasz już dwa małe przykłady GetFillPath metody SKPaint, która istnieje w dwóch wersjach:

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

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

Wymagane są tylko dwa pierwsze argumenty. Metoda uzyskuje dostęp do ścieżki przywoływanej src przez argument, modyfikuje dane ścieżki na podstawie właściwości pociągnięcia w SKPaint obiekcie (w tym PathEffect właściwości), a następnie zapisuje wyniki w ścieżce dst . Parametr resScale umożliwia zmniejszenie dokładności w celu utworzenia mniejszej ścieżki docelowej, a cullRect argument może wyeliminować kontury poza prostokątem.

Jedno z podstawowych zastosowań tej metody nie obejmuje w ogóle efektów ścieżki: jeśli SKPaint obiekt ma jej właściwość ustawioną na SKPaintStyle.Stroke, i nie ma PathEffect jego Style zestawu, tworzy GetFillPath ścieżkę reprezentującą kontur ścieżki źródłowej tak, jakby został pociągnięty przez właściwości farby.

Jeśli na przykład src ścieżka jest prostym okręgiem promienia 500, a SKPaint obiekt określa szerokość pociągnięcia 100, dst ścieżka staje się dwoma okręgami koncentrycznymi, jeden z promieniem 450, a drugi z promieniem 550. Metoda jest wywoływana GetFillPath , ponieważ wypełnienie tej dst ścieżki jest takie samo jak w przypadku postrojania ścieżki src . Można jednak również pociągnić ścieżkę dst , aby zobaczyć konspekt ścieżki.

Polecenie Naciśnij, aby oznaczyć ścieżkę , pokazuje to. Element SKCanvasView i TapGestureRecognizer są tworzone w pliku TapToOutlineThePathPage.xaml . Plik TapToOutlineThePathPage.xaml.cs kodu definiuje trzy SKPaint obiekty jako pola, dwa do strojenia z szerokościami pociągnięcia 100 i 20, a trzeci do wypełnienia:

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

Jeśli ekran nie został zamapowany, PaintSurface program obsługi używa blueFill obiektów i redThickStroke malowania do renderowania ścieżki cyklicznej:

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

Okrąg jest wypełniony i pociągnięta zgodnie z oczekiwaniami:

Potrójny zrzut ekranu przedstawiający normalne naciśnięcie, aby nakreślić stronę Ścieżka

Po naciśnięciu ekranu outlineThePath jest ustawiona wartość true, a PaintSurface program obsługi tworzy nowy SKPath obiekt i używa go jako ścieżki docelowej w wywołaniu GetFillPath obiektu farby redThickStroke . Ta ścieżka docelowa jest następnie wypełniana i pociągnięta redThinStrokeza pomocą metody , co powoduje wykonanie następujących czynności:

Potrójny zrzut ekranu przedstawiający zakreśloną stronę Naciśnij, aby nakreślić ścieżkę

Dwa czerwone okręgi wyraźnie wskazują, że oryginalna ścieżka okrągła została przekształcona w dwa okrągłe kontury.

Ta metoda może być bardzo przydatna podczas opracowywania ścieżek do użycia dla SKPathEffect.Create1DPath metody . Ścieżki określone w tych metodach są zawsze wypełniane, gdy ścieżki są replikowane. Jeśli nie chcesz wypełniać całej ścieżki, musisz dokładnie zdefiniować konspekty.

Na przykład w przykładzie Linked Chain linki zostały zdefiniowane z serią czterech łuków, z których każda została oparta na dwóch promieniach, aby nakreślić obszar ścieżki do wypełnienia. Można zastąpić kod w LinkedChainPage klasie, aby zrobić to nieco inaczej.

Najpierw należy ponownie zdefiniować stałą linkRadius :

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

Jest linkPath to teraz tylko dwa łuki oparte na tym pojedynczym promieniu, z żądanymi kątami rozpoczęcia i kątami zamiatania:

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

        }

    }
}

Obiekt outlinePath jest następnie adresatem konspektu po linkPath pociągnięciu za pomocą właściwości określonych w strokePaintpliku .

Inny przykład użycia tej techniki jest następny dla ścieżki używanej w metodzie.

Łączenie efektów ścieżki

Dwie ostatnie metody tworzenia statycznego elementu SKPathEffect to SKPathEffect.CreateSum i SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Obie te metody łączą dwa efekty ścieżki, aby utworzyć efekt ścieżki złożonej. Metoda CreateSum tworzy efekt ścieżki, który jest podobny do dwóch efektów ścieżki zastosowanych oddzielnie, podczas gdy CreateCompose stosuje jeden efekt ścieżki () inner, a następnie stosuje do tego.outer

Wiesz już, jak GetFillPath metoda SKPaint może przekonwertować jedną ścieżkę na inną ścieżkę na SKPaint podstawie właściwości (w tym PathEffect), więc nie powinna być zbyt tajemnicza, jak SKPaint obiekt może wykonać tę operację dwa razy z dwoma efektami ścieżki określonymi w CreateSum metodach lub CreateCompose .

Jednym z oczywistych CreateSum zastosowań jest zdefiniowanie SKPaint obiektu, który wypełnia ścieżkę jednym efektem ścieżki, a następnie uderza ścieżkę innym efektem ścieżki. Jest to pokazane w przykładzie Cats in Frame , który wyświetla tablicę kotów w ramce z przecętymi krawędziami:

Potrójny zrzut ekranu przedstawiający stronę Cats In Frame (Koty w ramce)

Klasa CatsInFramePage rozpoczyna się od zdefiniowania kilku pól. Możesz rozpoznać pierwsze pole z PathDataCatPage klasy z artykułu Dane ścieżki SVG. Druga ścieżka jest oparta na linii i łuku dla wzorca ramki:

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

Można catPath go użyć w metodzie SKPathEffect.Create2DPath , jeśli SKPaint właściwość object Style jest ustawiona na Stroke. catPath Jeśli jednak jest używany bezpośrednio w tym programie, cała głowa kota zostanie wypełniona, a wąsy nawet nie będą widoczne. (Wypróbuj!) Należy uzyskać konspekt tej ścieżki i użyć tego konspektu w metodzie SKPathEffect.Create2DPath .

Konstruktor wykonuje to zadanie. Najpierw stosuje dwa przekształcenia, aby catPath przenieść punkt (0, 0) do środka i skalować go w dół w dół. GetFillPath uzyskuje wszystkie kontury w outlinedCatPathobiekcie , a obiekt jest używany w wywołaniu SKPathEffect.Create2DPath . Czynniki skalowania w SKMatrix wartości są nieco większe niż poziomy i pionowy rozmiar kota, aby zapewnić mały bufor między kafelkami, podczas gdy czynniki tłumaczenia zostały uzyskane nieco empirycznie, aby pełny kot był widoczny w lewym górnym rogu ramki:

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

Następnie konstruktor wywołuje SKPathEffect.Create1DPath ramkę przegrzebną. Zwróć uwagę, że szerokość ścieżki wynosi 100 pikseli, ale postęp wynosi 75 pikseli, tak aby replikowana ścieżka nakładała się wokół ramki. Końcowa instrukcja konstruktora wywołuje SKPathEffect.CreateSum polecenie , aby połączyć dwa efekty ścieżki i ustawić wynik na SKPaint obiekt.

Ta praca pozwala programowi PaintSurface obsługi być dość proste. Musi tylko zdefiniować prostokąt i narysować go przy użyciu polecenia 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);
    }
}

Algorytmy stojące za efektami ścieżki zawsze powodują wyświetlanie całej ścieżki używanej do strojania lub wypełniania, co może spowodować, że niektóre wizualizacje pojawią się poza prostokątem. Wywołanie ClipRect przed DrawRect wywołaniem umożliwia znacznie czystsze wizualizacje. (Wypróbuj go bez wycinków!)

Często stosuje SKPathEffect.CreateCompose się, aby dodać jakiś roztrzasek do innego efektu ścieżki. Z pewnością możesz eksperymentować samodzielnie, ale oto nieco inny przykład:

Kreskowane kreskowane linie kreskowe wypełnia wielokropek liniami kreskowymi, które są kreskowane. Większość pracy w DashedHatchLinesPage klasie jest wykonywana bezpośrednio w definicjach pól. Te pola definiują efekt kreski i efekt kreskowania. Są one definiowane jako static , ponieważ są one następnie przywoływali w SKPathEffect.CreateCompose wywołaniu SKPaint w definicji:

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

Procedura PaintSurface obsługi musi zawierać tylko standardowe obciążenie i jedno wywołanie polecenia 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);
    }
    ...
}

Jak już wiesz, linie kreskowe nie są dokładnie ograniczone do wnętrza obszaru, a w tym przykładzie zawsze zaczynają się po lewej stronie z całą kreską:

Zrzut ekranu przedstawiający stronę Linia kreskowana kreskowana

Teraz, gdy już znasz efekty ścieżki, które wahają się od prostych kropek i kresek po dziwne kombinacje, użyj wyobraźni i zobacz, co możesz utworzyć.