Поделиться через


Данные пути SVG в SkiaSharp

Определение путей с помощью текстовых строк в формате масштабируемой векторной графики

Класс SKPath поддерживает определение всех объектов пути из текстовых строк в формате, установленном спецификацией Scalable Vector Graphics (SVG). Далее в этой статье вы увидите, как представить весь путь, например этот, в текстовой строке:

Пример пути, определенный с данными пути 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 объект, преобразовав ToSvgPathDataSKPath объект в строку.

Вот строка 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. SKPathSKPaint Оба объекта определяются как поля в 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 для другого типа перемещения. Эти функции упрощения приводят к тому, что вторая рука передвигается в рыжим режиме — оттягивается немного, прежде чем он движется, а затем немного перестрелки его назначения, эффект, который, к сожалению, не может быть воспроизведен в этих статических снимках экрана:

Тройной снимок экрана: страница