Сведения о пути и перечисление
Получение сведений о путях и перечислении содержимого
Класс SKPath
определяет несколько свойств и методов, позволяющих получить сведения о пути. TightBounds
Свойства Bounds
(и связанные методы) получают метрики пути. Этот Contains
метод позволяет определить, находится ли определенная точка в пути.
Иногда полезно определить общую длину всех линий и кривых, составляющих путь. Вычисление этой длины не является алгоритмически простой задачей, поэтому целый класс с именем PathMeasure
посвящен ему.
Иногда бывает полезно получить все операции рисования и точки, составляющие путь. Сначала это средство может показаться ненужным: если программа создала путь, программа уже знает содержимое. Однако вы видели, что пути также можно создавать с помощью эффектов пути и преобразовывать текстовые строки в пути. Вы также можете получить все операции и точки рисования, составляющие эти пути. Одна из возможностей заключается в применении алгоритмического преобразования ко всем точкам, например для упаковки текста вокруг полушария:
Получение длины пути
В статье "Пути и текст " вы узнали, как использовать DrawTextOnPath
метод для рисования текстовой строки, базовые показатели которой соответствуют курсу пути. Но что делать, если вы хотите размер текста таким образом, чтобы он точно соответствовал пути? Рисование текста вокруг круга легко, так как окружность круга проста для вычисления. Но окружность многоточия или длина кривой Bézier не так проста.
Класс SKPathMeasure
может помочь. Конструктор принимает SKPath
аргумент, и Length
свойство показывает его длину.
Этот класс демонстрируется в примере длины пути, основанном на странице "Кривая Bezier". Файл PathLengthPage.xaml является производным от InteractivePage
и включает сенсорный интерфейс:
<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>
Файл PathLengthPage.xaml.cs кода позволяет перемещать четыре точки касания для определения конечных точек и контрольных точек кубической кривой Bézier. Три поля определяют текстовую строку, SKPaint
объект и вычисляемую ширину текста:
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);
...
}
Поле baseTextWidth
— это ширина текста на TextSize
основе параметра 10.
Обработчик PaintSurface
рисует кривую Bézier, а затем размер текста, который будет соответствовать его полной длине:
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);
}
...
}
Свойство Length
только что созданного SKPathMeasure
объекта получает длину пути. Длина пути делится baseTextWidth
на значение (это ширина текста на основе размера текста 10), а затем умножается на базовый размер текста 10. Результатом является новый размер текста для отображения текста вдоль этого пути:
По мере того как кривая Bézier становится длиннее или короче, можно увидеть изменение размера текста.
Обход пути
SKPathMeasure
может сделать больше, чем просто измерять длину пути. Для любого значения от нуля до длины SKPathMeasure
пути объект может получить позицию по пути и тангенс к кривой пути в этой точке. Тангенс доступен в виде вектора в виде объекта или в виде поворота SKPoint
, инкапсулированного в SKMatrix
объекте. Ниже приведены методы SKPathMeasure
получения этой информации различными и гибкими способами:
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
:
GetPosition
GetTangent
GetPositionAndTangent
Страница Unicycle Half-Pipe анимирует фигуру палки на юницикле, который, кажется, ездить назад и вперед вдоль кубической кривой Bézier:
Объект SKPaint
, используемый для поглаживания как половины канала, так и юницикла, определяется как поле в UnicycleHalfPipePage
классе. Также определен объект для юницикла SKPath
:
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");
...
}
Класс содержит стандартные переопределения и OnDisappearing
методы OnAppearing
анимации. Обработчик PaintSurface
создает путь для половинного канала, а затем рисует его. Затем SKPathMeasure
объект создается на основе этого пути:
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);
}
}
}
}
Обработчик PaintSurface
вычисляет значение t
от 0 до 1 каждые пять секунд. Затем функция используется Math.Cos
для преобразования этого значения в диапазон t
от 0 до 1 и обратно в 0, где 0 соответствует юнициклу в начале слева, а 1 соответствует юнициклу в правом верхнем углу. Функция косинуса приводит к замедлению скорости в верхней части трубы и самой быстрой в нижней части.
Обратите внимание, что это значение t
должно умножаться на длину пути для первого аргумента GetMatrix
. Затем матрица применяется к SKCanvas
объекту для рисования пути юницикла.
Перечисление пути
Два внедренных класса SKPath
позволяют перечислять содержимое пути. Эти классы и SKPath.Iterator
SKPath.RawIterator
. Два класса очень похожи, но SKPath.Iterator
могут устранять элементы в пути с нулевой длиной или близко к нулевой длине. Используется RawIterator
в приведенном ниже примере.
Объект типа SKPath.RawIterator
можно получить, вызвав CreateRawIterator
метод SKPath
. Перечисление по пути выполняется путем многократного Next
вызова метода. Передайте в него массив из четырех SKPoint
значений:
SKPoint[] points = new SKPoint[4];
...
SKPathVerb pathVerb = rawIterator.Next(points);
Метод Next
возвращает элемент SKPathVerb
типа перечисления. Эти значения указывают определенную команду рисования в пути. Количество допустимых точек, вставленных в массив, зависит от этой команды:
Move
с одной точкойLine
с двумя точкамиCubic
с четырьмя точкамиQuad
с тремя точкамиConic
с тремя точками (а также вызовитеConicWeight
метод для веса)Close
с одной точкойDone
Команда указывает, что перечисление Done
пути завершено.
Обратите внимание, что нет Arc
глаголов. Это означает, что все дуги преобразуются в кривые Bézier при добавлении в путь.
Некоторые сведения в массиве SKPoint
избыточны. Например, если Move
за командой Line
следует глагол, то первая из двух точек, сопровождающих Line
ее, совпадает с Move
точкой. На практике эта избыточность очень полезна. При получении Cubic
команды он сопровождается всеми четырьмя точками, определяющими кубическую кривую Bézier. Текущее положение, установленное предыдущей командой, не требуется.
Проблематичная команда, однако, является Close
. Эта команда рисует прямую линию от текущей позиции к началу контура, установленного ранее командой Move
. В идеале Close
команда должна предоставлять эти два пункта, а не только одну точку. Хуже того, что точка, сопровождающая Close
глагол, всегда (0, 0). При перечислении по пути, вероятно, потребуется сохранить Move
точку и текущую позицию.
Перечисление, сплошение и неправильный формат
Иногда желательно применить алгоритмическое преобразование к пути к неправильной форме:
Большая часть этих букв состоит из прямых линий, но эти прямые линии, по-видимому, были закручены в кривые. Как это происходит?
Ключ заключается в том, что исходные прямые линии разбиваются на ряд небольших прямых линий. Затем эти отдельные небольшие прямые линии можно манипулировать различными способами формирования кривой.
Чтобы помочь в этом процессе, пример содержит статический PathExtensions
класс с Interpolate
методом, который разбивает прямую линию на многочисленные короткие строки, которые являются только одной единицей длины. Кроме того, класс содержит несколько методов, которые преобразуют три типа кривых Bézier в ряд крошечных прямых линий, которые приблизит кривую. (Формулы параметрики представлены в статье Три типа кривых Bézier.) Этот процесс называется плоской кривой:
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));
}
}
Все эти методы ссылаются на метод CloneWithTransform
расширения, также включены в этот класс и показаны ниже. Этот метод клонирует путь, перечисляя команды пути и создавая новый путь на основе данных. Однако новый путь состоит только из вызовов MoveTo
и LineTo
вызовов. Все кривые и прямые линии сокращаются до ряда крошечных линий.
При вызове CloneWithTransform
метод передается методу Func<SKPoint, SKPoint>
, который является функцией с SKPaint
параметром, возвращающим SKPoint
значение. Эта функция вызывается для каждой точки для применения пользовательского алгоритмического преобразования:
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;
}
...
}
Так как клонированные пути сокращаются до крошечных прямых линий, функция преобразования имеет возможность преобразования прямых линий в кривые.
Обратите внимание, что метод сохраняет первую точку каждого контура в переменной, вызываемой firstPoint
и текущей позиции после каждой команды рисования в переменной lastPoint
. Эти переменные необходимы для создания окончательной закрывающей строки при Close
обнаружении команды.
Пример GlobularText использует этот метод расширения, чтобы, казалось бы, упаковать текст вокруг полушария в трехмерном эффекте:
Конструктор GlobularTextPage
классов выполняет это преобразование. Он создает SKPaint
объект для текста, а затем получает SKPath
объект из GetTextPath
метода. Это путь, передаваемый методу CloneWithTransform
расширения вместе с функцией преобразования:
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);
});
}
}
}
...
}
Функция преобразования сначала вычисляет два значения с именем longitude
и latitude
диапазон от –π/2 в верхней и левой части текста до π/2 в правом и нижнем углу текста. Диапазон этих значений не является визуально удовлетворительно, поэтому они сокращаются путем умножения на 0,75. (Попробуйте код без этих настроек. Текст становится слишком неясным на северном и южном полюсах, и слишком тонким на сторонах.) Эти трехмерные сферические координаты преобразуются в двухмерные x
и y
координаты по стандартным формулам.
Новый путь хранится в виде поля. Затем PaintSurface
обработчику нужно только центрировать и масштабировать путь, чтобы отобразить его на экране:
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);
}
}
}
Это очень универсальный метод. Если массив эффектов пути, описанных в статье "Эффекты пути", не вполне охватывает то, что вы чувствовали, следует включить, это способ заполнить пробелы.