Данные пути SVG в SkiaSharp
Определение путей с помощью текстовых строк в формате масштабируемой векторной графики
Класс SKPath
поддерживает определение всех объектов пути из текстовых строк в формате, установленном спецификацией Scalable Vector Graphics (SVG). Далее в этой статье вы увидите, как представить весь путь, например этот, в текстовой строке:
SVG — это язык программирования графики на основе XML для веб-страниц. Так как SVG должен разрешать определение путей в разметке, а не ряд вызовов функций, стандарт SVG включает чрезвычайно краткий способ указания всего графического пути в виде текстовой строки.
В SkiaSharp этот формат называется "SVG path-data". Формат также поддерживается в средах программирования на основе Windows XAML, включая Windows Presentation Foundation и универсальная платформа Windows, где он называется синтаксисом разметки пути или синтаксисом команд перемещения и рисования. Он также может служить форматом обмена для векторных графических изображений, особенно в текстовых файлах, таких как XML.
Класс SKPath
определяет два метода со словами SvgPathData
в их именах:
public static SKPath ParseSvgPathData(string svgPath)
public string ToSvgPathData()
ParseSvgPathData
Статический метод преобразует строку в SKPath
объект, преобразовав ToSvgPathData
SKPath
объект в строку.
Вот строка SVG для пятиконечной звезды, сосредоточенной на точке (0, 0) с радиусом 100:
"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"
Буквы — это команды, которые создают SKPath
объект: M
указывает MoveTo
вызов, L
имеет значение LineTo
и Z
закрывает Close
контур. Каждая пара чисел предоставляет координату X и Y точки. Обратите внимание, что L
за командой следует несколько точек, разделенных запятыми. В ряде координат и точек запятые и пробелы обрабатываются одинаково. Некоторые программисты предпочитают запятыми между координатами X и Y, а не между точками, но запятые или пробелы требуются только для предотвращения неоднозначности. Это совершенно законно:
"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"
Синтаксис данных пути SVG официально описан в разделе 8.3 спецификации SVG. Ниже приведена сводка:
MoveTo
M x y
Это начинает новый контур в пути, задав текущее положение. Данные пути всегда должны начинаться с M
команды.
LineTo
L x y ...
Эта команда добавляет прямую строку (или строки) в путь и задает новое текущее положение в конце последней строки. Вы можете следовать команде L
с несколькими парами координат x и y .
Горизонтальная линияTo
H x ...
Эта команда добавляет горизонтальную строку в путь и задает новую текущую позицию в конце строки. Вы можете следовать команде H
с несколькими координатами x , но это не имеет большого смысла.
Вертикаль
V y ...
Эта команда добавляет вертикальную строку в путь и задает новое текущее положение в конце строки.
Закрыть
Z
Команда C
закрывает контур, добавив прямую линию от текущей позиции к началу контура.
ArcTo
Команда для добавления эллиптической дуги в контур является самой сложной командой во всей спецификации пути SVG. Это единственная команда, в которой числа могут представлять нечто, отличное от значений координат:
A rx ry rotation-angle large-arc-flag sweep-flag x y ...
Параметры rx и ry — это горизонтальные и вертикальные радии многоточия. Угол поворота по часовой стрелке в градусах.
Задайте для большого дуги значение 1 для большой дуги или 0 для небольшой дуги.
Задайте для флага очистки значение 1 для часовой стрелки и 0 для счетчика по часовой стрелке.
Дуга рисуется к точке (x, y), которая становится новой текущей позицией.
CubicTo
C x1 y1 x2 y2 x3 y3 ...
Эта команда добавляет кубическую кривую Bézier из текущей позиции в (x3, y3), которая становится новой текущей позицией. Точки управления (x1, y1) и (x2, y2) являются контрольным точками.
Несколько кривых Bézier можно указать одной C
командой. Число точек должно быть кратным из 3.
Существует также команда "smooth" Bézier кривой:
S x2 y2 x3 y3 ...
Эта команда должна соответствовать обычной команде Bézier (хотя это не обязательно). Команда smooth Bézier вычисляет первую контрольную точку, чтобы она была отражением второй контрольной точки предыдущего Bézier вокруг их взаимной точки. Эти три точки, следовательно, строгая, и связь между двумя кривыми Bézier является гладкой.
QuadTo
Q x1 y1 x2 y2 ...
Для квадратных кривых Bézier число точек должно быть кратным 2. Контрольная точка — (x1, y1) и конечная точка (и новая текущая позиция) — (x2, y2)
Существует также команда гладкой квадратной кривой:
T x2 y2 ...
Контрольная точка вычисляется на основе контрольной точки предыдущей квадратной кривой.
Все эти команды также доступны в "относительных" версиях, где точки координат относительно текущей позиции. Эти относительные команды начинаются с строчные буквы, например c
, а не C
для относительной версии команды кубической Bézier.
Это степень определения пути SVG. Не существует средства для повторяющихся групп команд или для выполнения любого типа вычисления. Команды для ConicTo
или других типов спецификаций arc недоступны.
SKPath.ParseSvgPathData
Статический метод ожидает допустимую строку команд SVG. Если обнаружена любая синтаксическая ошибка, метод возвращается null
. Это единственное указание ошибки.
Этот ToSvgPathData
метод удобно для получения данных пути SVG из существующего SKPath
объекта для передачи в другую программу или хранения в текстовом формате файла, например XML. (Метод ToSvgPathData
не демонстрируется в примере кода в этой статье.) Не ожидайте ToSvgPathData
возвращать строку, соответствующую вызовам метода, которые создали путь. В частности, вы обнаружите, что дуги преобразуются в несколько QuadTo
команд, и это то, как они отображаются в данных пути, возвращенных из ToSvgPathData
.
Страница Path Data Hello описывает слово HELLO с помощью данных пути SVG. SKPath
SKPaint
Оба объекта определяются как поля в PathDataHelloPage
классе:
public class PathDataHelloPage : ContentPage
{
SKPath helloPath = SKPath.ParseSvgPathData(
"M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100" + // H
"M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100" + // E
"M 150 0 L 150 100, 200 100" + // L
"M 225 0 L 225 100, 275 100" + // L
"M 300 50 A 25 50 0 1 0 300 49.9 Z"); // O
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 10,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
...
}
Путь, определяющий текстовую строку, начинается в левом верхнем углу в точке (0, 0). Каждая буква составляет 50 единиц ширины и 100 единиц высоты, а буквы разделены еще 25 единицами, что означает, что весь путь составляет 350 единиц.
"H" из "Hello" состоит из трех одностроковых контуров, в то время как "E" является двумя подключенными кубической кривой Bézier. Обратите внимание, что C
за командой следует шесть точек, а две из контрольных точек имеют координаты Y –10 и 110, которые помещают их за пределы диапазона координат Y других букв. L — это две подключенные линии, а "O" — это многоточие, которое отображается с A
помощью команды.
Обратите внимание, что M
команда, начинающаяся с последнего контура, устанавливает положение в точку (350, 50), которая является вертикальным центром левой стороны "O". Как указано в первых числах после A
команды, многоточие имеет горизонтальный радиус 25 и вертикальный радиус 50. Конечная точка обозначается последней парой чисел в A
команде, представляющей точку (300, 49.9). Это намеренно немного отличается от начальной точки. Если конечная точка равна начальной точке, то дуга не будет отображаться. Чтобы нарисовать полную многоточие, необходимо установить конечную точку близко к (но не равно) начальной точке или использовать две или более A
команд, каждая из которых входит в полную многоточие.
Может потребоваться добавить следующую инструкцию в конструктор страницы, а затем задать точку останова для проверки результирующей строки:
string str = helloPath.ToSvgPathData();
Вы узнаете, что дуга была заменена длинной рядом Q
команд для кусочной приближения дуги с помощью квадратной кривой Bézier.
Обработчик PaintSurface
получает жесткие границы пути, которые не включают контрольные точки для кривых E и O. Три преобразования перемещают центр пути к точке (0, 0), масштабируйте путь к размеру холста (но также с учетом ширины штриха), а затем переместите центр пути в центр холста:
public class PathDataHelloPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect bounds;
helloPath.GetTightBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(info.Width / (bounds.Width + paint.StrokeWidth),
info.Height / (bounds.Height + paint.StrokeWidth));
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPath(helloPath, paint);
}
}
Путь заполняет холст, который выглядит более разумно при просмотре в альбомном режиме:
Страница "Путь к данным cat" аналогична. Объекты пути и краски определяются как поля в PathDataCatPage
классе:
public class PathDataCatPage : ContentPage
{
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 paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Orange,
StrokeWidth = 5
};
...
}
Голова кота круг, и здесь он отрисовывается двумя A
командами, каждая из которых рисует точку с запятой. Обе A
команды для головы определяют горизонтальные и вертикальные радии 100. Первая дуга начинается с (240, 100) и заканчивается на (240, 300), которая становится начальной точкой для второй дуги, которая заканчивается обратно (240, 100).
Два глаза также отображаются с двумя A
командами, и как и с головой кота, вторая A
команда заканчивается в той же точке, что и начало первой A
команды. Однако эти пары A
команд не определяют многоточие. С каждой дугой 40 единиц, а радиус также составляет 40 единиц, что означает, что эти дуги не являются полными полуцирками.
Обработчик PaintSurface
выполняет аналогичные преобразования, как предыдущий пример, но задает один Scale
фактор для поддержания пропорции и обеспечивает небольшое поле, чтобы виски кота не касались сторон экрана:
public class PathDataCatPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Black);
SKRect bounds;
catPath.GetBounds(out bounds);
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(0.9f * Math.Min(info.Width / bounds.Width,
info.Height / bounds.Height));
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPath(catPath, paint);
}
}
Вот работающая программа:
Как правило, если SKPath
объект определен как поле, контуры пути должны быть определены в конструкторе или другом методе. Однако при использовании данных пути SVG видно, что путь можно указать полностью в определении поля.
Предыдущий пример аналоговых часов в статье "Преобразование поворота" отображает руки часов как простые строки. Следующая программа "Довольно аналоговые часы " заменяет эти строки объектами, SKPath
определенными как поля в PrettyAnalogClockPage
классе вместе с SKPaint
объектами:
public class PrettyAnalogClockPage : ContentPage
{
...
// Clock hands pointing straight up
SKPath hourHandPath = SKPath.ParseSvgPathData(
"M 0 -60 C 0 -30 20 -30 5 -20 L 5 0" +
"C 5 7.5 -5 7.5 -5 0 L -5 -20" +
"C -20 -30 0 -30 0 -60 Z");
SKPath minuteHandPath = SKPath.ParseSvgPathData(
"M 0 -80 C 0 -75 0 -70 2.5 -60 L 2.5 0" +
"C 2.5 5 -2.5 5 -2.5 0 L -2.5 -60" +
"C 0 -70 0 -75 0 -80 Z");
SKPath secondHandPath = SKPath.ParseSvgPathData(
"M 0 10 L 0 -80");
// SKPaint objects
SKPaint handStrokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 2,
StrokeCap = SKStrokeCap.Round
};
SKPaint handFillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Gray
};
...
}
Часовые и минутные руки теперь имеют закрытые области. Чтобы сделать эти руки отличными друг от друга, они рисуются как черным контуром, так и серым заливкой с помощью handStrokePaint
объектов и handFillPaint
объектов.
В предыдущем примере ugly Аналоговые часы маленькие круги, отмеченные часами и минутами, были нарисованы в цикле. В этом примере "Довольно аналоговые часы" используется совершенно другой подход: часовые и минутные знаки имеют пунктирные линии, рисуемые с minuteMarkPaint
помощью объектов:hourMarkPaint
public class PrettyAnalogClockPage : ContentPage
{
...
SKPaint minuteMarkPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 3,
StrokeCap = SKStrokeCap.Round,
PathEffect = SKPathEffect.CreateDash(new float[] { 0, 3 * 3.14159f }, 0)
};
SKPaint hourMarkPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 6,
StrokeCap = SKStrokeCap.Round,
PathEffect = SKPathEffect.CreateDash(new float[] { 0, 15 * 3.14159f }, 0)
};
...
}
В статье Dots и Dashes описано, как использовать SKPathEffect.CreateDash
метод для создания дефисной строки. Первый аргумент — это массив, который обычно содержит два элемента: первый элемент — float
длина дефисов, а второй элемент — разрыв между дефисами. StrokeCap
Если для свойства задано SKStrokeCap.Round
значение, то округленные концы тире фактически продлиют длину тире по ширине штриха на обеих сторонах тире. Таким образом, при задании первого элемента массива значение 0 создает пунктирную линию.
Расстояние между этими точками регулируется вторым элементом массива. Как вы увидите вскоре, эти два SKPaint
объекта используются для рисования кругов с радиусом 90 единиц. Таким образом, окружность этого круга составляет 180π, что означает, что каждые 60 минут должны отображаться каждые 3π единиц, что является вторым значением в массивеfloat
.minuteMarkPaint
12-часовые знаки должны отображаться каждые 15π единиц, что является значением во втором float
массиве.
Класс PrettyAnalogClockPage
задает таймер, чтобы сделать поверхность недопустимой каждые 16 миллисекунд, и PaintSurface
обработчик вызывается по этой скорости. Более ранние SKPath
определения объектов SKPaint
позволяют выполнять очень чистый код рисования:
public class PrettyAnalogClockPage : ContentPage
{
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Transform for 100-radius circle in center
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(Math.Min(info.Width / 200, info.Height / 200));
// Draw circles for hour and minute marks
SKRect rect = new SKRect(-90, -90, 90, 90);
canvas.DrawOval(rect, minuteMarkPaint);
canvas.DrawOval(rect, hourMarkPaint);
// Get time
DateTime dateTime = DateTime.Now;
// Draw hour hand
canvas.Save();
canvas.RotateDegrees(30 * dateTime.Hour + dateTime.Minute / 2f);
canvas.DrawPath(hourHandPath, handStrokePaint);
canvas.DrawPath(hourHandPath, handFillPaint);
canvas.Restore();
// Draw minute hand
canvas.Save();
canvas.RotateDegrees(6 * dateTime.Minute + dateTime.Second / 10f);
canvas.DrawPath(minuteHandPath, handStrokePaint);
canvas.DrawPath(minuteHandPath, handFillPaint);
canvas.Restore();
// Draw second hand
double t = dateTime.Millisecond / 1000.0;
if (t < 0.5)
{
t = 0.5 * Easing.SpringIn.Ease(t / 0.5);
}
else
{
t = 0.5 * (1 + Easing.SpringOut.Ease((t - 0.5) / 0.5));
}
canvas.Save();
canvas.RotateDegrees(6 * (dateTime.Second + (float)t));
canvas.DrawPath(secondHandPath, handStrokePaint);
canvas.Restore();
}
}
Что-то особенное делается со второй рукой, однако. Так как часы обновляются каждые 16 миллисекунд, Millisecond
свойство DateTime
значения может быть использовано для анимации смены секунды вместо того, чтобы перемещаться в дискретных прыжках с секунды на секунду. Но этот код не позволяет плавному перемещению. Вместо этого он использует Xamarin.FormsSpringIn
функции упрощения анимации SpringOut
для другого типа перемещения. Эти функции упрощения приводят к тому, что вторая рука передвигается в рыжим режиме — оттягивается немного, прежде чем он движется, а затем немного перестрелки его назначения, эффект, который, к сожалению, не может быть воспроизведен в этих статических снимках экрана: