Wyliczanie i informacje o ścieżce
Uzyskiwanie informacji o ścieżkach i wyliczanie zawartości
Klasa SKPath
definiuje kilka właściwości i metod, które umożliwiają uzyskanie informacji o ścieżce. Właściwości Bounds
i TightBounds
(i powiązane metody) uzyskują wymiary metrowe ścieżki. Metoda Contains
umożliwia określenie, czy określony punkt znajduje się w ścieżce.
Czasami przydatne jest określenie całkowitej długości wszystkich linii i krzywych tworzących ścieżkę. Obliczanie tej długości nie jest algorytmicznie prostym zadaniem, więc cała klasa o nazwie PathMeasure
jest mu poświęcona.
Czasami przydatne jest również uzyskanie wszystkich operacji rysowania i punktów tworzących ścieżkę. Na początku ta funkcja może wydawać się niepotrzebna: Jeśli program utworzył ścieżkę, program już zna zawartość. Wiesz jednak, że ścieżki mogą być również tworzone przez efekty ścieżki i przez konwertowanie ciągów tekstowych na ścieżki. Można również uzyskać wszystkie operacje i punkty rysunku, które tworzą te ścieżki. Jedną z możliwości jest zastosowanie przekształcenia algorytmicznego do wszystkich punktów, na przykład w celu opakowania tekstu wokół półkuli:
Pobieranie długości ścieżki
W artykule Ścieżki i tekst pokazano, jak za pomocą DrawTextOnPath
metody narysować ciąg tekstowy, którego punkt odniesienia jest zgodny z przebiegiem ścieżki. Ale co zrobić, jeśli chcesz rozmiar tekstu tak, aby dokładnie pasował do ścieżki? Rysowanie tekstu wokół okręgu jest łatwe, ponieważ obwód okręgu jest prosty do obliczenia. Ale obwód wielokropka lub długość krzywej Béziera nie jest tak prosta.
Klasa SKPathMeasure
może pomóc. Konstruktor akceptuje SKPath
argument, a Length
właściwość ujawnia jego długość.
Ta klasa jest pokazana w przykładzie Długość ścieżki , która jest oparta na stronie Krzywa Beziera. Plik PathLengthPage.xaml pochodzi z InteractivePage
pliku i zawiera interfejs dotykowy:
<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SkiaSharpFormsDemos"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Curves.PathLengthPage"
Title="Path Length">
<Grid BackgroundColor="White">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
</local:InteractivePage>
Plik PathLengthPage.xaml.cs kodu umożliwia przenoszenie czterech punktów dotykowych w celu zdefiniowania punktów końcowych i punktów kontrolnych krzywej Béziera sześciennego. Trzy pola definiują ciąg tekstowy, SKPaint
obiekt i szerokość obliczeniową tekstu:
public partial class PathLengthPage : InteractivePage
{
const string text = "Compute length of path";
static SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Black,
TextSize = 10,
};
static readonly float baseTextWidth = textPaint.MeasureText(text);
...
}
Pole baseTextWidth
jest szerokością tekstu na TextSize
podstawie ustawienia 10.
Program PaintSurface
obsługi rysuje krzywą Bézier, a następnie rozmiaruje tekst, aby zmieścił się wzdłuż pełnej długości:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Draw path with cubic Bezier curve
using (SKPath path = new SKPath())
{
path.MoveTo(touchPoints[0].Center);
path.CubicTo(touchPoints[1].Center,
touchPoints[2].Center,
touchPoints[3].Center);
canvas.DrawPath(path, strokePaint);
// Get path length
SKPathMeasure pathMeasure = new SKPathMeasure(path, false, 1);
// Find new text size
textPaint.TextSize = pathMeasure.Length / baseTextWidth * 10;
// Draw text on path
canvas.DrawTextOnPath(text, path, 0, 0, textPaint);
}
...
}
Właściwość Length
nowo utworzonego SKPathMeasure
obiektu uzyskuje długość ścieżki. Długość ścieżki jest podzielona przez baseTextWidth
wartość (czyli szerokość tekstu na podstawie rozmiaru tekstu 10), a następnie pomnożona przez rozmiar tekstu podstawowego 10. Wynik jest nowym rozmiarem tekstu do wyświetlania tekstu wzdłuż tej ścieżki:
Ponieważ krzywa Béziera jest dłuższa lub krótsza, można zobaczyć zmianę rozmiaru tekstu.
Przechodzenie ścieżki
SKPathMeasure
może zrobić więcej niż tylko zmierzyć długość ścieżki. W przypadku dowolnej wartości z zakresu od zera do długości SKPathMeasure
ścieżki obiekt może uzyskać pozycję na ścieżce i tangens do krzywej ścieżki w tym momencie. Tangens jest dostępny jako wektor w postaci SKPoint
obiektu lub jako obrót hermetyzowany w SKMatrix
obiekcie. Poniżej przedstawiono metody uzyskiwania SKPathMeasure
tych informacji w różny i elastyczny sposób:
Boolean GetPosition (Single distance, out SKPoint position)
Boolean GetTangent (Single distance, out SKPoint tangent)
Boolean GetPositionAndTangent (Single distance, out SKPoint position, out SKPoint tangent)
Boolean GetMatrix (Single distance, out SKMatrix matrix, SKPathMeasureMatrixFlags flag)
SKPathMeasureMatrixFlags
Członkowie wyliczenia to:
GetPosition
GetTangent
GetPositionAndTangent
Strona Unicycle Half-Pipe animuje figurę kija na unicycle, który wydaje się jeździć tam iz powrotem wzdłuż sześciennej krzywej Béziera:
Obiekt SKPaint
używany do strojania zarówno pół potoku, jak i mononicycle jest definiowany jako pole w UnicycleHalfPipePage
klasie. Zdefiniowany również obiekt jest obiektem SKPath
dla jednokleru:
public class UnicycleHalfPipePage : ContentPage
{
...
SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
StrokeWidth = 3,
Color = SKColors.Black
};
SKPath unicyclePath = SKPath.ParseSvgPathData(
"M 0 0" +
"A 25 25 0 0 0 0 -50" +
"A 25 25 0 0 0 0 0 Z" +
"M 0 -25 L 0 -100" +
"A 15 15 0 0 0 0 -130" +
"A 15 15 0 0 0 0 -100 Z" +
"M -25 -85 L 25 -85");
...
}
Klasa zawiera standardowe przesłonięcia OnAppearing
metod i OnDisappearing
dla animacji. Procedura PaintSurface
obsługi tworzy ścieżkę dla potoku pół potoku, a następnie rysuje ją. Następnie SKPathMeasure
obiekt jest tworzony na podstawie tej ścieżki:
public class UnicycleHalfPipePage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPath pipePath = new SKPath())
{
pipePath.MoveTo(50, 50);
pipePath.CubicTo(0, 1.25f * info.Height,
info.Width - 0, 1.25f * info.Height,
info.Width - 50, 50);
canvas.DrawPath(pipePath, strokePaint);
using (SKPathMeasure pathMeasure = new SKPathMeasure(pipePath))
{
float length = pathMeasure.Length;
// Animate t from 0 to 1 every three seconds
TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
float t = (float)(timeSpan.TotalSeconds % 5 / 5);
// t from 0 to 1 to 0 but slower at beginning and end
t = (float)((1 - Math.Cos(t * 2 * Math.PI)) / 2);
SKMatrix matrix;
pathMeasure.GetMatrix(t * length, out matrix,
SKPathMeasureMatrixFlags.GetPositionAndTangent);
canvas.SetMatrix(matrix);
canvas.DrawPath(unicyclePath, strokePaint);
}
}
}
}
Procedura PaintSurface
obsługi oblicza wartość z przedziału t
od 0 do 1 co pięć sekund. Następnie używa Math.Cos
funkcji , aby przekonwertować ją na wartość t
z zakresu od 0 do 1 i z powrotem do 0, gdzie 0 odpowiada unicycle na początku po lewej stronie górnej, podczas gdy 1 odpowiada unicycle w prawym górnym rogu. Cosinus funkcja powoduje spowolnienie prędkości u góry rury i najszybsze na dole.
Zwróć uwagę, że ta wartość t
musi być pomnożona przez długość ścieżki dla pierwszego argumentu na GetMatrix
wartość . Macierz jest następnie stosowana do SKCanvas
obiektu w celu rysowania ścieżki unicycle.
Wyliczanie ścieżki
Dwie osadzone klasy SKPath
umożliwiają wyliczanie zawartości ścieżki. Te klasy to SKPath.Iterator
i SKPath.RawIterator
. Dwie klasy są bardzo podobne, ale SKPath.Iterator
mogą wyeliminować elementy w ścieżce o zerowej długości lub blisko zerowej długości. Element RawIterator
jest używany w poniższym przykładzie.
Obiekt typu SKPath.RawIterator
można uzyskać, wywołując metodę CreateRawIterator
SKPath
. Wyliczanie ścieżki odbywa się przez wielokrotne wywoływanie Next
metody . Przekaż do niej tablicę czterech SKPoint
wartości:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
Metoda Next
zwraca element członkowski SKPathVerb
typu wyliczenia. Te wartości wskazują konkretne polecenie rysunku w ścieżce. Liczba prawidłowych punktów wstawionych w tablicy zależy od tego zlecenia:
Move
z pojedynczym punktemLine
z dwoma punktamiCubic
z czterema punktamiQuad
z trzema punktamiConic
z trzema punktami (a także wywołaj metodęConicWeight
wagi)Close
z jednym punktemDone
Czasownik Done
wskazuje, że wyliczenie ścieżki zostało ukończone.
Zwróć uwagę, że nie Arc
ma żadnych czasowników. Oznacza to, że wszystkie łuki są konwertowane na krzywe Bézier po dodaniu do ścieżki.
Niektóre informacje w tablicy SKPoint
są nadmiarowe. Jeśli na przykład Move
czasownik jest po Line
nim czasownik, pierwszy z dwóch punktów towarzyszących Line
jest taki sam jak Move
punkt. W praktyce ta nadmiarowość jest bardzo pomocna. Gdy otrzymujesz Cubic
czasownik, towarzyszy mu wszystkie cztery punkty definiujące krzywą Sześciennego Béziera. Nie musisz zachować bieżącego położenia ustalonego przez poprzednie zlecenie.
Problematyczne czasowniki to Close
jednak . To polecenie rysuje linię prostą z bieżącej pozycji na początek konturu ustalonego Move
wcześniej przez polecenie . W idealnym przypadku czasownik powinien podać te dwa punkty, Close
a nie tylko jeden punkt. Co gorsza, punkt towarzyszący Close
czasownikowi jest zawsze (0, 0). Podczas wyliczania ścieżki prawdopodobnie trzeba zachować Move
punkt i bieżącą pozycję.
Wyliczanie, spłaszczanie i źle sformułowanie
Czasami pożądane jest zastosowanie transformacji algorytmicznej do ścieżki, aby ją źle sformułować w jakiś sposób:
Większość tych liter składa się z prostych linii, ale te linie proste najwyraźniej zostały skręcone w krzywe. Jak to możliwe?
Kluczem jest to, że oryginalne linie proste są podzielone na serię mniejszych linii prostych. Te mniejsze linie proste można następnie manipulować na różne sposoby, aby utworzyć krzywą.
Aby ułatwić ten proces, przykład zawiera klasę statyczną PathExtensions
z Interpolate
metodą, która dzieli linię prostą na wiele krótkich wierszy, które są tylko jedną jednostką długości. Ponadto klasa zawiera kilka metod, które konwertują trzy typy krzywych Bézier na serię małych linii prostych, które przybliżają krzywą. (Formuły parametryczne zostały przedstawione w artykule Trzy typy krzywych Béziera. Ten proces jest nazywany spłaszczanie krzywej:
static class PathExtensions
{
...
static SKPoint[] Interpolate(SKPoint pt0, SKPoint pt1)
{
int count = (int)Math.Max(1, Length(pt0, pt1));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * pt0.X + t * pt1.X;
float y = (1 - t) * pt0.Y + t * pt1.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
3 * t * (1 - t) * (1 - t) * pt1.X +
3 * t * t * (1 - t) * pt2.X +
t * t * t * pt3.X;
float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
3 * t * (1 - t) * (1 - t) * pt1.Y +
3 * t * t * (1 - t) * pt2.Y +
t * t * t * pt3.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenQuadratic(SKPoint pt0, SKPoint pt1, SKPoint pt2)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
points[i] = new SKPoint(x, y);
}
return points;
}
static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight)
{
int count = (int)Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
SKPoint[] points = new SKPoint[count];
for (int i = 0; i < count; i++)
{
float t = (i + 1f) / count;
float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
x /= denominator;
y /= denominator;
points[i] = new SKPoint(x, y);
}
return points;
}
static double Length(SKPoint pt0, SKPoint pt1)
{
return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
}
}
Wszystkie te metody są przywołyne z metody CloneWithTransform
rozszerzenia również zawarte w tej klasie i pokazane poniżej. Ta metoda klonuje ścieżkę, wyliczając polecenia ścieżki i tworząc nową ścieżkę na podstawie danych. Jednak nowa ścieżka składa się tylko z MoveTo
wywołań i LineTo
. Wszystkie krzywe i linie proste są zredukowane do serii małych linii.
Podczas wywoływania CloneWithTransform
metody należy przekazać metodę Func<SKPoint, SKPoint>
, która jest funkcją z parametrem SKPaint
, który zwraca SKPoint
wartość. Ta funkcja jest wywoływana dla każdego punktu w celu zastosowania niestandardowej transformacji algorytmicznej:
static class PathExtensions
{
public static SKPath CloneWithTransform(this SKPath pathIn, Func<SKPoint, SKPoint> transform)
{
SKPath pathOut = new SKPath();
using (SKPath.RawIterator iterator = pathIn.CreateRawIterator())
{
SKPoint[] points = new SKPoint[4];
SKPathVerb pathVerb = SKPathVerb.Move;
SKPoint firstPoint = new SKPoint();
SKPoint lastPoint = new SKPoint();
while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done)
{
switch (pathVerb)
{
case SKPathVerb.Move:
pathOut.MoveTo(transform(points[0]));
firstPoint = lastPoint = points[0];
break;
case SKPathVerb.Line:
SKPoint[] linePoints = Interpolate(points[0], points[1]);
foreach (SKPoint pt in linePoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[1];
break;
case SKPathVerb.Cubic:
SKPoint[] cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]);
foreach (SKPoint pt in cubicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[3];
break;
case SKPathVerb.Quad:
SKPoint[] quadPoints = FlattenQuadratic(points[0], points[1], points[2]);
foreach (SKPoint pt in quadPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Conic:
SKPoint[] conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight());
foreach (SKPoint pt in conicPoints)
{
pathOut.LineTo(transform(pt));
}
lastPoint = points[2];
break;
case SKPathVerb.Close:
SKPoint[] closePoints = Interpolate(lastPoint, firstPoint);
foreach (SKPoint pt in closePoints)
{
pathOut.LineTo(transform(pt));
}
firstPoint = lastPoint = new SKPoint(0, 0);
pathOut.Close();
break;
}
}
}
return pathOut;
}
...
}
Ponieważ sklonowana ścieżka jest ograniczona do małych linii prostych, funkcja przekształcania ma możliwość konwertowania linii prostych na krzywe.
Zwróć uwagę, że metoda zachowuje pierwszy punkt każdego konturu w zmiennej o nazwie firstPoint
i bieżące położenie po każdym poleceniu rysunku w zmiennej lastPoint
. Te zmienne są niezbędne do konstruowania końcowego wiersza zamknięcia, gdy Close
napotkano czasownik.
Przykład GlobularText używa tej metody rozszerzenia, aby pozornie zawijać tekst wokół półkuli w efekt 3D:
Konstruktor GlobularTextPage
klasy wykonuje tę transformację. SKPaint
Tworzy obiekt dla tekstu, a następnie uzyskuje SKPath
obiekt z GetTextPath
metody . Jest to ścieżka przekazana do CloneWithTransform
metody rozszerzenia wraz z funkcją transform:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
public GlobularTextPage()
{
Title = "Globular Text";
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
using (SKPaint textPaint = new SKPaint())
{
textPaint.Typeface = SKTypeface.FromFamilyName("Times New Roman");
textPaint.TextSize = 100;
using (SKPath textPath = textPaint.GetTextPath("HELLO", 0, 0))
{
SKRect textPathBounds;
textPath.GetBounds(out textPathBounds);
globePath = textPath.CloneWithTransform((SKPoint pt) =>
{
double longitude = (Math.PI / textPathBounds.Width) *
(pt.X - textPathBounds.Left) - Math.PI / 2;
double latitude = (Math.PI / textPathBounds.Height) *
(pt.Y - textPathBounds.Top) - Math.PI / 2;
longitude *= 0.75;
latitude *= 0.75;
float x = (float)(Math.Cos(latitude) * Math.Sin(longitude));
float y = (float)Math.Sin(latitude);
return new SKPoint(x, y);
});
}
}
}
...
}
Funkcja transform najpierw oblicza dwie wartości o nazwie longitude
i latitude
od –π/2 w górnej i lewej części tekstu, aby π/2 po prawej i dolnej części tekstu. Zakres tych wartości nie jest wizualnie zadowalający, dlatego są one zmniejszane przez pomnożenie przez 0,75. (Wypróbuj kod bez tych korekt. Tekst staje się zbyt niejasny na biegunach północnych i południowych i zbyt cienkich po bokach). Te trójwymiarowe współrzędne sfericzne są konwertowane na dwuwymiarowe x
i y
współrzędne według standardowych formuł.
Nowa ścieżka jest przechowywana jako pole. Następnie PaintSurface
program obsługi musi jedynie wyśrodkować i skalować ścieżkę, aby wyświetlić ją na ekranie:
public class GlobularTextPage : ContentPage
{
SKPath globePath;
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
using (SKPaint pathPaint = new SKPaint())
{
pathPaint.Style = SKPaintStyle.Fill;
pathPaint.Color = SKColors.Blue;
pathPaint.StrokeWidth = 3;
pathPaint.IsAntialias = true;
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.45f * Math.Min(info.Width, info.Height)); // radius
canvas.DrawPath(globePath, pathPaint);
}
}
}
Jest to bardzo wszechstronna technika. Jeśli tablica efektów ścieżki opisanych w artykule Path Effects nie obejmuje czegoś, co należy uwzględnić, jest to sposób na wypełnienie luk.