SkiaSharp의 SVG 경로 데이터

확장 가능한 벡터 그래픽 형식의 텍스트 문자열을 사용하여 경로 정의

클래스는 SKPath SVG(Scalable Vector Graphics) 사양에 의해 설정된 형식으로 텍스트 문자열의 전체 경로 개체 정의를 지원합니다. 이 문서의 뒷부분에서 텍스트 문자열에서 이 경로와 같은 전체 경로를 나타내는 방법을 확인할 수 있습니다.

SVG 경로 데이터로 정의된 샘플 경로

SVG는 웹 페이지에 대한 XML 기반 그래픽 프로그래밍 언어입니다. SVG는 일련의 함수 호출이 아닌 태그에서 경로를 정의하도록 허용해야 하므로 SVG 표준에는 전체 그래픽 경로를 텍스트 문자열로 지정하는 매우 간결한 방법이 포함되어 있습니다.

SkiaSharp 내에서 이 형식을 "SVG 경로 데이터"라고 합니다. 이 형식은 Windows Presentation Foundation 및 경로 태그 구문 또는 이동 및 그리기 명령 구문이라고 하는 유니버설 Windows 플랫폼 포함하여 Windows XAML 기반 프로그래밍 환경에서도 지원됩니다. 특히 XML과 같은 텍스트 기반 파일에서 벡터 그래픽 이미지의 교환 형식으로 사용할 수도 있습니다.

클래스는 SKPath 이름에 단어가 SvgPathData 포함된 두 가지 메서드를 정의합니다.

public static SKPath ParseSvgPathData(string svgPath)

public string ToSvgPathData()

정적 ParseSvgPathData 메서드는 문자열을 개체로 SKPath 변환하는 동시에 ToSvgPathData 개체를 SKPath 문자열로 변환합니다.

다음은 반경이 100인 점(0, 0)을 중심으로 한 5개의 뾰족한 별에 대한 SVG 문자열입니다.

"M 0 -100 L 58.8 90.9, -95.1 -30.9, 95.1 -30.9, -58.8 80.9 Z"

문자는 개체를 빌드 SKPath 하는 명령입니다. 즉, LineToL 호출을 MoveTo 나타내며 ZClose, 윤곽을 닫습니다. M 각 숫자 쌍은 점의 X 및 Y 좌표를 제공합니다. L 명령 뒤에는 여러 지점이 쉼표로 구분됩니다. 일련의 좌표와 점에서 쉼표와 공백은 동일하게 처리됩니다. 일부 프로그래머는 점 사이에 있는 것이 아니라 X 좌표와 Y 좌표 사이에 쉼표를 배치하는 것을 선호하지만 모호성을 방지하기 위해서는 쉼표나 공백만 있으면 됩니다. 이것은 완벽하게 합법적입니다:

"M0-100L58.8 90.9-95.1-30.9 95.1-30.9-58.8 80.9Z"

SVG 경로 데이터의 구문은 SVG 사양의 섹션 8.3에 공식적으로 문서화되어 있습니다. 요약은 다음과 같습니다.

Moveto

M x y

현재 위치를 설정하여 경로에서 새 윤곽선이 시작됩니다. 경로 데이터는 항상 명령으로 M 시작해야 합니다.

Lineto

L x y ...

이 명령은 경로에 직선(또는 선)을 추가하고 새 현재 위치를 마지막 줄의 끝으로 설정합니다. 여러 쌍의 x 및 y 좌표로 명령을 따를 L 수 있습니다.

가로 LineTo

H x ...

이 명령은 경로에 가로 선을 추가하고 새 현재 위치를 줄의 끝으로 설정합니다. 여러 x 좌표로 H 명령을 따를 수 있지만 별로 의미가 없습니다.

세로선

V y ...

이 명령은 경로에 세로 선을 추가하고 새 현재 위치를 줄의 끝으로 설정합니다.

닫기

Z

C 명령은 현재 위치에서 윤곽선의 시작 부분에 직선을 추가하여 윤곽선을 닫습니다.

ArcTo

윤곽선에 타원형 호를 추가하는 명령은 전체 SVG 경로 데이터 사양에서 가장 복잡한 명령입니다. 숫자가 좌표 값이 아닌 다른 항목을 나타낼 수 있는 유일한 명령입니다.

A rx ry rotation-angle large-arc-flag sweep-flag x y ...

rxry 매개 변수는 타원의 가로 및 세로 반지름입니다. 회전 각도는 시계 방향으로 도 단위입니다.

큰 호의 경우 큰 호 플래그를 1로, 작은 호의 경우 0으로 설정합니다.

시계 방향의 경우 스윕 플래그 를 1로, 시계 반대 방향으로는 0으로 설정합니다.

호는 새로운 현재 위치가 되는 점(x, y)에 그려집니다.

입방토

C x1 y1 x2 y2 x3 y3 ...

이 명령은 현재 위치에서 (x3, y3)에 입방형 베지어 곡선을 추가하여 새 현재 위치가 됩니다. 점(x1, y1) 및 (x2, y2)는 제어점입니다.

단일 C 명령으로 여러 Bézier 곡선을 지정할 수 있습니다. 점 수는 3의 배수여야 합니다.

"부드러운" 베지어 곡선 명령도 있습니다.

S x2 y2 x3 y3 ...

이 명령은 일반 Bézier 명령을 따라야 합니다(반드시 필요한 것은 아니지만). 부드러운 Bézier 명령은 첫 번째 제어점을 계산하여 상호 지점 주위의 이전 Bézier의 두 번째 제어점을 반영합니다. 따라서 이 세 가지 점은 콜린어이며 두 베지어 곡선 간의 연결은 매끄럽습니다.

QuadTo

Q x1 y1 x2 y2 ...

이차 베지어 곡선의 경우 점 수는 2의 배수여야 합니다. 제어점은 (x1, y1)이고 끝점(및 새 현재 위치)은(x2, y2)

부드러운 이차 곡선 명령도 있습니다.

T x2 y2 ...

제어점은 이전 이차 곡선의 제어점을 기반으로 계산됩니다.

이러한 모든 명령은 좌표점이 현재 위치를 기준으로 하는 "상대" 버전에서도 사용할 수 있습니다. 이러한 상대 명령은 예를 들어 c 입방형 Bézier 명령의 상대 버전이 아닌 C 소문자로 시작합니다.

이는 SVG 경로-데이터 정의의 범위입니다. 명령 그룹을 반복하거나 모든 유형의 계산을 수행할 수 있는 기능은 없습니다. 또는 다른 유형의 호 사양에 대한 ConicTo 명령을 사용할 수 없습니다.

정적 SKPath.ParseSvgPathData 메서드에는 유효한 SVG 명령 문자열이 필요합니다. 구문 오류가 발견되면 메서드가 반환됩니다 null. 이것이 유일한 오류 표시입니다.

ToSvgPathData 메서드는 기존 SKPath 개체에서 SVG 경로 데이터를 가져와서 다른 프로그램으로 전송하거나 XML과 같은 텍스트 기반 파일 형식으로 저장하는 데 편리합니다. (이 메서드는 ToSvgPathData 이 문서의 샘플 코드에 설명되어 있지 않습니다.) 경로를 만든 메서드 호출에 정확히 해당하는 문자열을 반환할 필요가 ToSvgPathData 없습니다. 특히 호가 여러 QuadTo 명령으로 변환되고 반환된 경로 데이터에 ToSvgPathData표시되는 방식을 확인할 수 있습니다.

경로 데이터 Hello 페이지는 SVG 경로 데이터를 사용하여 "HELLO"라는 단어를 철자합니다. 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단원입니다.

"Hello"의 'H'는 3개의 한 줄 윤곽선으로 구성되며, 'E'는 연결된 두 개의 입방형 베지어 곡선입니다. 명령 뒤에 6개의 점이 C 있고, 제어점 중 2개에는 Y 좌표가 –10과 110으로 지정되어 다른 문자의 Y 좌표 범위를 벗어나게 됩니다. 'L'은 두 개의 연결된 선이지만 'O'는 명령으로 A 렌더링되는 줄임표입니다.

M 마지막 윤곽선이 시작되는 명령은 위치를 'O'의 왼쪽 세로 중심인 점(350, 50)으로 설정합니다. 명령 다음 A 의 첫 번째 숫자로 표시된 것처럼 줄임표의 가로 반경은 25이고 세로 반경은 50입니다. 끝점은 지점(300, 49.9)을 나타내는 명령의 마지막 숫자 A 쌍으로 표시됩니다. 이는 의도적으로 시작점과 약간 다릅니다. 엔드포인트가 시작점과 동일하게 설정되면 호가 렌더링되지 않습니다. 전체 줄임표를 그리려면 끝점을 시작점에 가깝게 설정하거나 전체 줄임표의 일부에 대해 각각 두 개 이상의 A 명령을 사용해야 합니다.

다음 문을 페이지의 생성자에 추가한 다음 중단점을 설정하여 결과 문자열을 검사할 수 있습니다.

string str = helloPath.ToSvgPathData();

호가 이차 베지어 곡선을 사용하여 아크의 Q 증분 근사치에 대한 긴 일련의 명령으로 대체되었음을 알 수 있습니다.

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

경로는 캔버스를 채우며 가로 모드에서 볼 때 더 합리적입니다.

경로 데이터 Hello 페이지의 삼중 스크린샷

경로 데이터 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 명령으로 렌더링되며, 고양이의 머리와 마찬가지로 두 번째 명령은 첫 번째 AA 명령의 시작과 동일한 지점에서 끝납니다. 그러나 이러한 명령 쌍은 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 경로 데이터를 사용하는 경우 경로가 필드 정의에 완전히 지정될 수 있음을 확인했습니다.

회전 변환 문서의 이전 추한 아날로그 시계 샘플은 시계의 손을 간단한 선으로 표시했습니다. 아래의 Pretty 아날로그 시계 프로그램은 이러한 줄을 개체와 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
    };
    ...
}

이제 시간과 분 손에는 밀폐 된 영역이 있습니다. 이러한 손을 서로 구별하기 위해 검은색 윤곽선과 회색 채우기가 모두와 개체를 handStrokePainthandFillPaint 사용하여 그려집니다.

이전 의 못생긴 아날로그 시계 샘플에서는 시간과 분을 표시한 작은 원을 루프에 그렸습니다. 이 Pretty 아날로그 시계 샘플에서는 완전히 다른 접근 방식이 사용됩니다. 시간 및 분 표시는 점선과 hourMarkPaint 개체로 minuteMarkPaint 그려집니다.

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

점 및 대시 문서에서는 메서드를 사용하여 SKPathEffect.CreateDash 파선 생성 방법을 설명했습니다. 첫 번째 인수는 float 일반적으로 두 개의 요소가 있는 배열입니다. 첫 번째 요소는 대시의 길이이고 두 번째 요소는 대시 사이의 간격입니다. 속성이 StrokeCap 설정된 SKStrokeCap.Round경우 대시의 둥근 끝은 대시 양쪽의 스트로크 너비로 대시 길이를 효과적으로 깁니다. 따라서 첫 번째 배열 요소를 0으로 설정하면 점선이 만들어집니다.

이러한 점 사이의 거리는 두 번째 배열 요소에 의해 제어됩니다. 곧 볼 수 있듯이 이 두 SKPaint 개체는 반경이 90단원인 원을 그리는 데 사용됩니다. 따라서 이 원의 둘레는 180π입니다. 즉, 60분 표시는 배열의 floatminuteMarkPaint두 번째 값인 3π 단위마다 나타나야 합니다. 12시간 표시는 두 번째 float 배열의 값인 15π 단위마다 나타나야 합니다.

클래스는 PrettyAnalogClockPage 16밀리초마다 표면을 무효화하도록 타이머를 설정하고 처리 PaintSurface 기는 해당 속도로 호출됩니다. 및 개체의 SKPathSKPaint 이전 정의는 매우 클린 그리기 코드를 허용합니다.

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.FormsSpringInSpringOut 애니메이션 감속/가속 함수를 사용합니다. 이러한 감속/가속 함수는 초침이 이동하기 전에 조금 뒤로 당기고 대상을 약간 과도하게 촬영하여 이러한 정적 스크린샷에서 재현할 수 없는 효과를 발생시키는 것입니다.

프리티 아날로그 시계 페이지의 삼중 스크린샷